checkpoints revert/restore diffareas!!!

This commit is contained in:
Andrew Pareles 2025-04-03 17:12:18 -07:00
parent bde51106a1
commit a2a9cdba60
10 changed files with 290 additions and 269 deletions

View file

@ -30,6 +30,8 @@ import { IVoidModelService } from '../common/voidModelService.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { findLastIdx } from '../../../../base/common/arraysFind.js';
import { IEditCodeService } from './editCodeServiceInterface.js';
import { VoidFileSnapshot } from '../common/editCodeServiceTypes.js';
/*
@ -238,6 +240,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
@IMetricsService private readonly _metricsService: IMetricsService,
@IEditorService private readonly _editorService: IEditorService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IEditCodeService private readonly _editCodeService: IEditCodeService,
// @IModelService private readonly _modelService: IModelService,
) {
@ -787,13 +790,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const { model } = this._voidModelService.getModel(uri)
if (!model) return // should never happen
const currValue = model.getValue() // afterStr = the value of the file right after the edit
const diffAreasSnapshot = this._editCodeService.getVoidFileSnapshot(uri)
this._addCheckpoint(threadId, {
role: 'checkpoint',
type: 'tool_edit',
beforeStrOfURI: { [uri.fsPath]: currValue, },
userModifications: { beforeStrOfURI: {} },
voidFileSnapshotOfURI: { [uri.fsPath]: diffAreasSnapshot },
userModifications: { voidFileSnapshotOfURI: {} },
})
}
@ -821,13 +824,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
private _computeNeededCheckpointChanges({ threadId }: { threadId: string }) {
private _computeCheckpointInfo({ threadId }: { threadId: string }) {
const thread = this.state.allThreads[threadId]
if (!thread) return
const { currCheckpointIdx } = thread.state
if (currCheckpointIdx === null) return
const currStrOfFsPath: { [fsPath: string]: string | undefined } = {}
const voidFileSnapshotOfURI: { [fsPath: string]: VoidFileSnapshot | undefined } = {}
// add a change for all the URIs in the checkpoint history
const { lastIdxOfURI } = this._getCheckpointsBetween({ threadId, loIdx: 0, hiIdx: currCheckpointIdx, }) ?? {}
@ -837,10 +840,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const checkpoint2 = thread.messages[lastIdxOfURI[fsPath]] || null
if (!checkpoint2) continue
if (checkpoint2.role !== 'checkpoint') continue
const oldStr = this._getBeforeStrAtCheckpoint(checkpoint2, fsPath, { includeUserModifiedChanges: false })
const currStr = model.getValue()
if (oldStr === currStr) continue
currStrOfFsPath[fsPath] = currStr
const res = this._getCheckpointInfo(checkpoint2, fsPath, { includeUserModifiedChanges: false })
if (!res) continue
const { voidFileSnapshot: oldVoidFileSnapshot } = res
// if there was any change to the str or diffAreaSnapshot, update. rough approximation of equality, oldDiffAreasSnapshot === diffAreasSnapshot is not perfect
const voidFileSnapshot = this._editCodeService.getVoidFileSnapshot(URI.file(fsPath))
if (oldVoidFileSnapshot === voidFileSnapshot) continue
voidFileSnapshotOfURI[fsPath] = voidFileSnapshot
}
// // add a change for all user-edited files (that aren't in the history)
@ -851,27 +858,30 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// currStrOfFsPath[fsPath] = model.getValue()
// }
return currStrOfFsPath
return { voidFileSnapshotOfURI }
}
// call this right before user sends message or reverts
private _addUserCheckpoint({ threadId }: { threadId: string }) {
const changes = this._computeNeededCheckpointChanges({ threadId })
const { voidFileSnapshotOfURI } = this._computeCheckpointInfo({ threadId }) ?? {}
this._addCheckpoint(threadId, {
role: 'checkpoint',
type: 'user_edit',
beforeStrOfURI: changes ?? {},
userModifications: { beforeStrOfURI: {} },
voidFileSnapshotOfURI: voidFileSnapshotOfURI ?? {},
userModifications: {
voidFileSnapshotOfURI: {},
},
})
}
private _addUserModificationsToCurrCheckpoint({ threadId }: { threadId: string }) {
const changes = this._computeNeededCheckpointChanges({ threadId })
const { voidFileSnapshotOfURI } = this._computeCheckpointInfo({ threadId }) ?? {}
const res = this._getCurrentCheckpoint(threadId)
if (!res) return
const [checkpoint, checkpointIdx] = res
this._editMessageInThread(threadId, checkpointIdx, {
...checkpoint,
userModifications: { beforeStrOfURI: changes ?? {} },
userModifications: { voidFileSnapshotOfURI: voidFileSnapshotOfURI ?? {}, },
})
}
@ -908,29 +918,30 @@ class ChatThreadService extends Disposable implements IChatThreadService {
for (let i = loIdx; i <= hiIdx; i += 1) {
const message = thread.messages[i]
if (message.role !== 'checkpoint') continue
for (const fsPath in message.beforeStrOfURI) { // do not include userModified.beforeStrOfURI here, jumping should not include those changes
for (const fsPath in message.voidFileSnapshotOfURI) { // do not include userModified.beforeStrOfURI here, jumping should not include those changes
lastIdxOfURI[fsPath] = i
}
}
return { lastIdxOfURI }
}
private _getBeforeStrAtCheckpoint = (checkpointMessage: ChatMessage & { role: 'checkpoint' }, fsPath: string, opts: { includeUserModifiedChanges: boolean }) => {
const beforeStr = fsPath in checkpointMessage.beforeStrOfURI ? checkpointMessage.beforeStrOfURI[fsPath] ?? null : null
if (!opts.includeUserModifiedChanges) return beforeStr
const userModifiedBeforeStr = fsPath in checkpointMessage.userModifications.beforeStrOfURI ? checkpointMessage.userModifications.beforeStrOfURI[fsPath] ?? null : null
return userModifiedBeforeStr ?? beforeStr
private _getCheckpointInfo = (checkpointMessage: ChatMessage & { role: 'checkpoint' }, fsPath: string, opts: { includeUserModifiedChanges: boolean }) => {
const voidFileSnapshot = checkpointMessage.voidFileSnapshotOfURI ? checkpointMessage.voidFileSnapshotOfURI[fsPath] ?? null : null
if (!opts.includeUserModifiedChanges) { return { voidFileSnapshot, } }
const userModifiedVoidFileSnapshot = fsPath in checkpointMessage.userModifications.voidFileSnapshotOfURI ? checkpointMessage.userModifications.voidFileSnapshotOfURI[fsPath] ?? null : null
return { voidFileSnapshot: userModifiedVoidFileSnapshot ?? voidFileSnapshot, }
}
private _writeFullFile = ({ fsPath, text }: { fsPath: string, text: string }) => {
const { model } = this._voidModelService.getModelFromFsPath(fsPath)
if (!model) return // should never happen
model.applyEdits([{
range: { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER }, // whole file
text
}])
}
// private _writeFullFile = ({ fsPath, text }: { fsPath: string, text: string }) => {
// const { model } = this._voidModelService.getModelFromFsPath(fsPath)
// if (!model) return // should never happen
// model.applyEdits([{
// range: { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER }, // whole file
// text
// }])
// }
jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified }: { threadId: string, messageIdx: number, jumpToUserModified: boolean }) {
const thread = this.state.allThreads[threadId]
@ -978,11 +989,12 @@ We only need to do it for files that were edited since `to`, ie files between to
for (let k = toIdx; k >= 0; k -= 1) {
const message = thread.messages[k]
if (message.role !== 'checkpoint') continue
const beforeStr = this._getBeforeStrAtCheckpoint(message, fsPath, { includeUserModifiedChanges: jumpToUserModified })
if (beforeStr !== null) {
this._writeFullFile({ fsPath, text: beforeStr })
break
}
const res = this._getCheckpointInfo(message, fsPath, { includeUserModifiedChanges: jumpToUserModified })
if (!res) continue
const { voidFileSnapshot } = res
if (!voidFileSnapshot) continue
this._editCodeService.restoreVoidFileSnapshot(URI.file(fsPath), voidFileSnapshot)
break
}
}
}
@ -1011,11 +1023,13 @@ We only need to do it for files that were edited since `from`, ie files between
for (let k = toIdx; k >= fromIdx + 1; k -= 1) {
const message = thread.messages[k]
if (message.role !== 'checkpoint') continue
const beforeStr = this._getBeforeStrAtCheckpoint(message, fsPath, { includeUserModifiedChanges: jumpToUserModified })
if (beforeStr !== null) {
this._writeFullFile({ fsPath, text: beforeStr })
break
}
const res = this._getCheckpointInfo(message, fsPath, { includeUserModifiedChanges: jumpToUserModified })
if (!res) continue
const { voidFileSnapshot } = res
if (!voidFileSnapshot) continue
this._editCodeService.restoreVoidFileSnapshot(URI.file(fsPath), voidFileSnapshot)
break
}
}
}

View file

@ -11,7 +11,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/brows
// import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
// import { throttle } from '../../../../base/common/decorators.js';
import { ComputedDiff, findDiffs } from './helpers/findDiffs.js';
import { findDiffs } from './helpers/findDiffs.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';
@ -40,13 +40,14 @@ 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 { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts } from './editCodeServiceInterface.js';
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts, } from './editCodeServiceInterface.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { FeatureName } from '../common/voidSettingsTypes.js';
import { IVoidModelService } from '../common/voidModelService.js';
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
import { deepClone } from '../../../../base/common/objects.js';
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
@ -107,7 +108,6 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number
};
// finds block.orig in fileContents and return its range in file
// startingAtLine is 1-indexed and inclusive
const findTextInCode = (text: string, fileContents: string, startingAtLine?: number) => {
@ -127,108 +127,6 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num
// // TODO diffArea should be removed if we just discovered it has no more diffs in it
// for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
// const diffArea = this.diffAreaOfId[diffareaid]
// if (Object.keys(diffArea._diffOfId).length === 0 && !diffArea._sweepState.isStreaming) {
// const { onFinishEdit } = this._addToHistory(uri)
// this._deleteDiffArea(diffArea)
// onFinishEdit()
// }
// }
export type Diff = {
diffid: number;
diffareaid: number; // the diff area this diff belongs to, "computed"
} & ComputedDiff
// _ means anything we don't include if we clone it
// DiffArea.originalStartLine is the line in originalCode (not the file)
type CommonZoneProps = {
diffareaid: number;
startLine: number;
endLine: number;
_URI: URI; // typically we get the URI from model
}
type CtrlKZone = {
type: 'CtrlKZone';
originalCode?: undefined;
editorId: string; // the editor the input lives on
_mountInfo: null | {
textAreaRef: { current: HTMLTextAreaElement | null }
dispose: () => void;
refresh: () => void;
}
_linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
} & CommonZoneProps
export type DiffZone = {
type: 'DiffZone',
originalCode: string;
_diffOfId: Record<string, Diff>; // diffid -> diff in this DiffArea
_streamState: {
isStreaming: true;
streamRequestIdRef: { current: string | null };
line: number;
} | {
isStreaming: false;
streamRequestIdRef?: undefined;
line?: undefined;
};
editorId?: undefined;
linkedStreamingDiffZone?: undefined;
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
} & CommonZoneProps
type TrackingZone<T> = {
type: 'TrackingZone';
metadata: T;
originalCode?: undefined;
editorId?: undefined;
_removeStylesFns?: undefined;
} & CommonZoneProps
// called DiffArea for historical purposes, we can rename to something like TextRegion if we want
export type DiffArea = CtrlKZone | DiffZone | TrackingZone<any>
const diffAreaSnapshotKeys = [
'type',
'diffareaid',
'originalCode',
'startLine',
'endLine',
'editorId',
] as const satisfies (keyof DiffArea)[]
type DiffAreaSnapshot<DiffAreaType extends DiffArea = DiffArea> = Pick<DiffAreaType, typeof diffAreaSnapshotKeys[number]>
type HistorySnapshot = {
snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot>;
entireFileCode: string;
}
// line/col is the location, originalCodeStartLine is the start line of the original code being displayed
type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }
@ -259,7 +157,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// ctrlKZone: [uri], isStreaming // listen on change streaming
private readonly _onDidChangeStreamingInCtrlKZone = new Emitter<{ uri: URI; diffareaid: number }>();
onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event
onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event;
constructor(
@ -722,98 +620,98 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _getCurrentVoidFileSnapshot = (uri: URI): VoidFileSnapshot => {
const { model } = this._voidModelService.getModel(uri)
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshotEntry> = {}
for (const diffareaid in this.diffAreaOfId) {
const diffArea = this.diffAreaOfId[diffareaid]
if (diffArea._URI.fsPath !== uri.fsPath) continue
snapshottedDiffAreaOfId[diffareaid] = deepClone(
Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]]))
) as DiffAreaSnapshotEntry
}
const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : ''
// this._noLongerNeedModelReference(uri)
return {
snapshottedDiffAreaOfId,
entireFileCode, // the whole file's code
}
}
private _restoreVoidFileSnapshot = async (uri: URI, snapshot: VoidFileSnapshot) => {
// for each diffarea in this uri, stop streaming if currently streaming
for (const diffareaid in this.diffAreaOfId) {
const diffArea = this.diffAreaOfId[diffareaid]
if (diffArea.type === 'DiffZone')
this._stopIfStreaming(diffArea)
}
// delete all diffareas on this uri (clearing their styles)
this._deleteAllDiffAreas(uri)
const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot
// restore diffAreaOfId and diffAreasOfModelId
for (const diffareaid in snapshottedDiffAreaOfId) {
const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid]
if (snapshottedDiffArea.type === 'DiffZone') {
this.diffAreaOfId[diffareaid] = {
...snapshottedDiffArea as DiffAreaSnapshotEntry<DiffZone>,
type: 'DiffZone',
_diffOfId: {},
_URI: uri,
_streamState: { isStreaming: false }, // when restoring, we will never be streaming
_removeStylesFns: new Set(),
}
}
else if (snapshottedDiffArea.type === 'CtrlKZone') {
this.diffAreaOfId[diffareaid] = {
...snapshottedDiffArea as DiffAreaSnapshotEntry<CtrlKZone>,
_URI: uri,
_removeStylesFns: new Set<Function>(),
_mountInfo: null,
_linkedStreamingDiffZone: null, // when restoring, we will never be streaming
}
}
this._addOrInitializeDiffAreaAtURI(uri, diffareaid)
}
this._onDidAddOrDeleteDiffZones.fire({ uri })
// restore file content
this._writeURIText(uri, entireModelCode,
'wholeFileRange',
{ shouldRealignDiffAreas: false }
)
// this._noLongerNeedModelReference(uri)
}
private _addToHistory(uri: URI, opts?: { onWillUndo?: () => void }) {
const getCurrentSnapshot = (): HistorySnapshot => {
const { model } = this._voidModelService.getModel(uri)
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot> = {}
for (const diffareaid in this.diffAreaOfId) {
const diffArea = this.diffAreaOfId[diffareaid]
if (diffArea._URI.fsPath !== uri.fsPath) continue
snapshottedDiffAreaOfId[diffareaid] = deepClone(
Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]]))
) as DiffAreaSnapshot
}
const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : ''
// this._noLongerNeedModelReference(uri)
return {
snapshottedDiffAreaOfId,
entireFileCode, // the whole file's code
}
}
const restoreDiffAreas = async (snapshot: HistorySnapshot) => {
// for each diffarea in this uri, stop streaming if currently streaming
for (const diffareaid in this.diffAreaOfId) {
const diffArea = this.diffAreaOfId[diffareaid]
if (diffArea.type === 'DiffZone')
this._stopIfStreaming(diffArea)
}
// delete all diffareas on this uri (clearing their styles)
this._deleteAllDiffAreas(uri)
this.diffAreasOfURI[uri.fsPath]?.clear()
const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot
// restore diffAreaOfId and diffAreasOfModelId
for (const diffareaid in snapshottedDiffAreaOfId) {
const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid]
if (snapshottedDiffArea.type === 'DiffZone') {
this.diffAreaOfId[diffareaid] = {
...snapshottedDiffArea as DiffAreaSnapshot<DiffZone>,
type: 'DiffZone',
_diffOfId: {},
_URI: uri,
_streamState: { isStreaming: false }, // when restoring, we will never be streaming
_removeStylesFns: new Set(),
}
}
else if (snapshottedDiffArea.type === 'CtrlKZone') {
this.diffAreaOfId[diffareaid] = {
...snapshottedDiffArea as DiffAreaSnapshot<CtrlKZone>,
_URI: uri,
_removeStylesFns: new Set<Function>(),
_mountInfo: null,
_linkedStreamingDiffZone: null, // when restoring, we will never be streaming
}
}
this._addOrInitializeDiffAreaAtURI(uri, diffareaid)
}
this._onDidAddOrDeleteDiffZones.fire({ uri })
// restore file content
this._writeURIText(uri, entireModelCode,
'wholeFileRange',
{ shouldRealignDiffAreas: false }
)
// this._noLongerNeedModelReference(uri)
}
const beforeSnapshot: HistorySnapshot = getCurrentSnapshot()
let afterSnapshot: HistorySnapshot | null = null
const beforeSnapshot: VoidFileSnapshot = this._getCurrentVoidFileSnapshot(uri)
let afterSnapshot: VoidFileSnapshot | null = null
const elt: IUndoRedoElement = {
type: UndoRedoElementType.Resource,
resource: uri,
label: 'Void Agent',
code: 'undoredo.editCode',
undo: () => { opts?.onWillUndo?.(); restoreDiffAreas(beforeSnapshot); },
redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) }
undo: () => { opts?.onWillUndo?.(); this._restoreVoidFileSnapshot(uri, beforeSnapshot); },
redo: () => { if (afterSnapshot) this._restoreVoidFileSnapshot(uri, afterSnapshot) }
}
this._undoRedoService.pushElement(elt)
const onFinishEdit = async () => {
afterSnapshot = getCurrentSnapshot()
afterSnapshot = this._getCurrentVoidFileSnapshot(uri)
await this._textFileService.save(uri, { // we want [our change] -> [save] so it's all treated as one change.
skipSaveParticipants: true // avoid triggering extensions etc (if they reformat the page, it will add another item to the undo stack)
})
@ -822,6 +720,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
public getVoidFileSnapshot(uri: URI) {
return this._getCurrentVoidFileSnapshot(uri)
}
public restoreVoidFileSnapshot(uri: URI, snapshot: VoidFileSnapshot): void {
this._restoreVoidFileSnapshot(uri, snapshot)
}
// delete diffOfId and diffArea._diffOfId
private _deleteDiff(diff: Diff) {
const diffArea = this.diffAreaOfId[diff.diffareaid]
@ -886,6 +794,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
else if (diffArea.type === 'CtrlKZone')
this._deleteCtrlKZone(diffArea)
})
this.diffAreasOfURI[uri.fsPath]?.clear()
}
private _addOrInitializeDiffAreaAtURI = (uri: URI, diffareaid: string | number) => {

View file

@ -7,8 +7,7 @@ import { Event } from '../../../../base/common/event.js';
import { URI } from '../../../../base/common/uri.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { Diff, DiffArea } from './editCodeService.js';
import { Diff, DiffArea, VoidFileSnapshot } from '../common/editCodeServiceTypes.js';
export type StartBehavior = 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts'
@ -32,8 +31,6 @@ export type StartApplyingOpts = {
startBehavior: StartBehavior;
}
export type AddCtrlKOpts = {
startLine: number,
endLine: number,
@ -70,4 +67,6 @@ export interface IEditCodeService {
interruptURIStreaming(opts: { uri: URI }): void;
// testDiffs(): void;
getVoidFileSnapshot(uri: URI): VoidFileSnapshot;
restoreVoidFileSnapshot(uri: URI, snapshot: VoidFileSnapshot): void;
}

View file

@ -3,34 +3,9 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ComputedDiff } from '../../common/editCodeServiceTypes.js';
import { diffLines } from '../react/out/diff/index.js'
export type ComputedDiff = {
type: 'edit';
originalCode: string;
originalStartLine: number;
originalEndLine: number;
code: string;
startLine: number; // 1-indexed
endLine: number;
} | {
type: 'insertion';
// originalCode: string;
originalStartLine: number; // insertion starts on column 0 of this
// originalEndLine: number;
code: string;
startLine: number;
endLine: number;
} | {
type: 'deletion';
originalCode: string;
originalStartLine: number;
originalEndLine: number;
// code: string;
startLine: number; // deletion starts on column 0 of this
// endLine: number;
}
export function findDiffs(oldStr: string, newStr: string) {
// this makes it so the end of the file always ends with a \n (if you don't have this, then diffing E vs E\n gives an "edit". With it, you end up diffing E\n vs E\n\n which now properly gives an insertion)

View file

@ -1,3 +1,8 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useState, useEffect, useCallback } from 'react'
import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
import { usePromise, useRefState } from '../util/helpers.js'
@ -144,7 +149,6 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u
const getStreamState = useCallback(() => {
const uri = getUriBeingApplied(applyBoxId)
console.log('uri',uri?.fsPath)
if (!uri) return 'idle-no-changes'
return voidCommandBarService.getStreamState(uri)
}, [voidCommandBarService, applyBoxId])
@ -162,7 +166,6 @@ export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, u
}, [applyBoxId, applyBoxId, uri]))
const currStreamState = getStreamState()
console.log('curr stream state', currStreamState)
return {
getStreamState,

View file

@ -1,7 +1,3 @@
//!!!! merged
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
@ -1821,12 +1817,12 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
const Checkpoint = ({ threadId, messageIdx }: { threadId: string; messageIdx: number }) => {
const accessor = useAccessor()
const chatThreadService = accessor.get('IChatThreadService')
const commandBarService = accessor.get('IVoidCommandBarService')
// const commandBarService = accessor.get('IVoidCommandBarService')
return <div
className='pointer-events-auto cursor-pointer select-none hover:brightness-125 flex items-center justify-center'
onClick={() => {
// reject all current changes and then jump back
commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' })
// commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' })
chatThreadService.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
}}>
<div className='bg-void-border-1 h-[1px] flex-grow'></div>

View file

@ -22,7 +22,6 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
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 { IEditCodeService } from '../../../editCodeServiceInterface.js'
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
@ -47,6 +46,7 @@ import { IVoidModelService } from '../../../../common/voidModelService.js'
import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'
import { IVoidCommandBarService } from '../../../voidCommandBarService.js'
import { INativeHostService } from '../../../../../../../platform/native/common/native.js';
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes

View file

@ -1,5 +1,11 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../base/common/uri.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
import { AnthropicReasoning } from './sendLLMMessageTypes.js';
import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
@ -29,11 +35,11 @@ export type ToolRequestApproval<T extends ToolName> = {
export type CheckpointEntry = {
role: 'checkpoint';
type: 'user_edit' | 'tool_edit';
beforeStrOfURI: { [fsPath: string]: string | undefined };
voidFileSnapshotOfURI: { [fsPath: string]: VoidFileSnapshot | undefined };
userModifications: {
beforeStrOfURI: { [fsPath: string]: string | undefined };
voidFileSnapshotOfURI: { [fsPath: string]: VoidFileSnapshot | undefined };
};
// diffAreas: null;
}

View file

@ -0,0 +1,119 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../base/common/uri.js';
export type ComputedDiff = {
type: 'edit';
originalCode: string;
originalStartLine: number;
originalEndLine: number;
code: string;
startLine: number; // 1-indexed
endLine: number;
} | {
type: 'insertion';
// originalCode: string;
originalStartLine: number; // insertion starts on column 0 of this
// originalEndLine: number;
code: string;
startLine: number;
endLine: number;
} | {
type: 'deletion';
originalCode: string;
originalStartLine: number;
originalEndLine: number;
// code: string;
startLine: number; // deletion starts on column 0 of this
// endLine: number;
}
// ---------- Diff types ----------
export type CommonZoneProps = {
diffareaid: number;
startLine: number;
endLine: number;
_URI: URI; // typically we get the URI from model
}
export type CtrlKZone = {
type: 'CtrlKZone';
originalCode?: undefined;
editorId: string; // the editor the input lives on
// _ means anything we don't include if we clone it
_mountInfo: null | {
textAreaRef: { current: HTMLTextAreaElement | null }
dispose: () => void;
refresh: () => void;
}
_linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
} & CommonZoneProps
export type TrackingZone<T> = {
type: 'TrackingZone';
metadata: T;
originalCode?: undefined;
editorId?: undefined;
_removeStylesFns?: undefined;
} & CommonZoneProps
// called DiffArea for historical purposes, we can rename to something like TextRegion if we want
export type DiffArea = CtrlKZone | DiffZone | TrackingZone<any>
export type Diff = {
diffid: number;
diffareaid: number; // the diff area this diff belongs to, "computed"
} & ComputedDiff
export type DiffZone = {
type: 'DiffZone',
originalCode: string;
_diffOfId: Record<string, Diff>; // diffid -> diff in this DiffArea
_streamState: {
isStreaming: true;
streamRequestIdRef: { current: string | null };
line: number;
} | {
isStreaming: false;
streamRequestIdRef?: undefined;
line?: undefined;
};
editorId?: undefined;
linkedStreamingDiffZone?: undefined;
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
} & CommonZoneProps
export const diffAreaSnapshotKeys = [
'type',
'diffareaid',
'originalCode',
'startLine',
'endLine',
'editorId',
] as const satisfies (keyof DiffArea)[]
export type DiffAreaSnapshotEntry<DiffAreaType extends DiffArea = DiffArea> = Pick<DiffAreaType, typeof diffAreaSnapshotKeys[number]>
export type VoidFileSnapshot = {
snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshotEntry>;
entireFileCode: string;
}

View file

@ -128,7 +128,7 @@ Here's an example of a good description:\n${editToolDescription}.`
name: 'terminal_command',
description: `Executes a terminal command.`,
params: {
command: { type: 'string', description: 'The terminal command to execute.' },
command: { type: 'string', description: 'The terminal command to execute. Typically you should pipe to cat to avoid pagination.' },
waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` },
terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' },
},