Skip to content

Commit e6636fd

Browse files
committed
nes: feat: support next cursor line prediction
not well tested due to 404 from the model
1 parent cd1487b commit e6636fd

File tree

3 files changed

+137
-4
lines changed

3 files changed

+137
-4
lines changed

src/extension/xtab/common/promptCrafting.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { illegalArgument } from '../../../util/vs/base/common/errors';
1616
import { Schemas } from '../../../util/vs/base/common/network';
1717
import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';
1818
import { StringText } from '../../../util/vs/editor/common/core/text/abstractText';
19+
import { CurrentDocument } from './xtabCurrentDocument';
1920

2021
export namespace PromptTags {
2122
export const CURSOR = "<|cursor|>";
@@ -147,6 +148,9 @@ export const xtab275SystemPrompt = `Predict the next code edit based on user con
147148

148149
export class PromptPieces {
149150
constructor(
151+
public readonly currentDocument: CurrentDocument,
152+
public readonly editWindowLinesRange: OffsetRange,
153+
public readonly areaAroundEditWindowLinesRange: OffsetRange,
150154
public readonly activeDoc: StatelessNextEditDocument,
151155
public readonly xtabHistory: readonly IXtabHistoryEntry[],
152156
public readonly currentFileContent: string,

src/extension/xtab/node/xtabProvider.ts

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { IWorkspaceService } from '../../../platform/workspace/common/workspaceS
3434
import { raceFilter } from '../../../util/common/async';
3535
import * as errors from '../../../util/common/errors';
3636
import { Result } from '../../../util/common/result';
37+
import { TokenizerType } from '../../../util/common/tokenizer';
3738
import { createTracer, ITracer } from '../../../util/common/tracing';
3839
import { AsyncIterableObject, DeferredPromise, raceTimeout, timeout } from '../../../util/vs/base/common/async';
3940
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
@@ -287,6 +288,9 @@ export class XtabProvider implements IStatelessNextEditProvider {
287288
}
288289

289290
const promptPieces = new PromptPieces(
291+
currentDocument,
292+
editWindowLinesRange,
293+
areaAroundEditWindowLinesRange,
290294
activeDocument,
291295
request.xtabEditHistory,
292296
taggedCurrentFileContent,
@@ -332,6 +336,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
332336
cursorOriginalLinesOffset,
333337
cursorLineOffset,
334338
editWindowLinesRange,
339+
promptPieces,
335340
prediction,
336341
{
337342
shouldRemoveCursorTagFromResponse,
@@ -351,7 +356,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
351356
currentDocument: CurrentDocument,
352357
editWindowLinesRange: OffsetRange,
353358
areaAroundEditWindowLinesRange: OffsetRange,
354-
promptOptions: ModelConfig,
359+
promptOptions: xtabPromptOptions.PromptOptions,
355360
computeTokens: (s: string) => number,
356361
opts: {
357362
includeLineNumbers: boolean;
@@ -533,6 +538,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
533538
cursorOriginalLinesOffset: number,
534539
cursorLineOffset: number, // cursor offset within the line it's in; 1-based
535540
editWindowLineRange: OffsetRange,
541+
promptPieces: PromptPieces,
536542
prediction: Prediction | undefined,
537543
opts: {
538544
promptingStrategy: xtabPromptOptions.PromptingStrategy | undefined;
@@ -679,7 +685,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
679685
const trimmedLines = firstLine.value.trim();
680686

681687
if (trimmedLines === ResponseTags.NO_CHANGE.start) {
682-
this.pushNoSuggestionsOrRetry(request, editWindow, pushEdit, delaySession, logContext, cancellationToken, telemetryBuilder, opts.retryState);
688+
await this.pushNoSuggestionsOrRetry(request, editWindow, promptPieces, pushEdit, delaySession, logContext, cancellationToken, telemetryBuilder, opts.retryState);
683689
return;
684690
}
685691

@@ -807,7 +813,7 @@ export class XtabProvider implements IStatelessNextEditProvider {
807813
if (hadEdits) {
808814
pushEdit(Result.error(new NoNextEditReason.NoSuggestions(request.documentBeforeEdits, editWindow)));
809815
} else {
810-
this.pushNoSuggestionsOrRetry(request, editWindow, pushEdit, delaySession, logContext, cancellationToken, telemetryBuilder, opts.retryState);
816+
await this.pushNoSuggestionsOrRetry(request, editWindow, promptPieces, pushEdit, delaySession, logContext, cancellationToken, telemetryBuilder, opts.retryState);
811817
}
812818

813819
} catch (err) {
@@ -818,9 +824,10 @@ export class XtabProvider implements IStatelessNextEditProvider {
818824
})();
819825
}
820826

821-
private pushNoSuggestionsOrRetry(
827+
private async pushNoSuggestionsOrRetry(
822828
request: StatelessNextEditRequest,
823829
editWindow: OffsetRange,
830+
promptPieces: PromptPieces,
824831
pushEdit: PushEdit,
825832
delaySession: DelaySession,
826833
logContext: InlineEditRequestLogContext,
@@ -836,6 +843,17 @@ export class XtabProvider implements IStatelessNextEditProvider {
836843
return;
837844
}
838845

846+
// FIXME@ulugbekna: think out how it works with retrying logic
847+
if (this.configService.getExperimentBasedConfig(ConfigKey.Internal.InlineEditsNextCursorPredictionEnabled, this.expService)) {
848+
// FIXME@ulugbekna: possibly convert from 1-based to 0-based
849+
const nextCursorLine = await this.predictNextCursorPosition(promptPieces);
850+
if (nextCursorLine) {
851+
this.tracer.trace(`Predicted next cursor line: ${nextCursorLine}`);
852+
this.doGetNextEditWithSelection(request, new Range(nextCursorLine, 1, nextCursorLine, 1), pushEdit, delaySession, logContext, cancellationToken, telemetryBuilder, RetryState.NotRetrying);
853+
return;
854+
}
855+
}
856+
839857
pushEdit(Result.error(new NoNextEditReason.NoSuggestions(request.documentBeforeEdits, editWindow)));
840858
return;
841859
}
@@ -996,6 +1014,113 @@ export class XtabProvider implements IStatelessNextEditProvider {
9961014
return sourcedModelConfig;
9971015
}
9981016

1017+
private async predictNextCursorPosition(promptPieces: PromptPieces) {
1018+
1019+
const tracer = this.tracer.sub('predictNextCursorPosition');
1020+
1021+
const systemMessage = 'Your task is to predict the next line number in the current file where the developer is most likely to make their next edit, using the provided context.';
1022+
1023+
const currentFileContentR = this.constructTaggedFile(
1024+
promptPieces.currentDocument,
1025+
promptPieces.editWindowLinesRange,
1026+
promptPieces.areaAroundEditWindowLinesRange,
1027+
promptPieces.opts,
1028+
XtabProvider.computeTokens,
1029+
{ includeLineNumbers: true }
1030+
);
1031+
1032+
if (currentFileContentR.isError()) {
1033+
tracer.trace(`Failed to construct tagged file: ${currentFileContentR.err}`);
1034+
return;
1035+
}
1036+
1037+
const { taggedCurrentFileR: { taggedCurrentFileContent }, areaAroundCodeToEdit } = currentFileContentR.val;
1038+
1039+
const newPromptPieces = new PromptPieces(
1040+
promptPieces.currentDocument,
1041+
promptPieces.editWindowLinesRange,
1042+
promptPieces.areaAroundEditWindowLinesRange,
1043+
promptPieces.activeDoc,
1044+
promptPieces.xtabHistory,
1045+
taggedCurrentFileContent,
1046+
areaAroundCodeToEdit,
1047+
promptPieces.langCtx,
1048+
XtabProvider.computeTokens,
1049+
promptPieces.opts,
1050+
);
1051+
1052+
const userMessage = getUserPrompt(newPromptPieces);
1053+
1054+
const messages = constructMessages({
1055+
systemMsg: systemMessage,
1056+
userMsg: userMessage
1057+
});
1058+
1059+
const modelName = this.configService.getExperimentBasedConfig(ConfigKey.Internal.InlineEditsNextCursorPredictionModelName, this.expService);
1060+
if (modelName === undefined) {
1061+
tracer.trace('Model name for cursor prediction is not defined; skipping prediction');
1062+
return;
1063+
}
1064+
1065+
const url = this.configService.getConfig(ConfigKey.Internal.InlineEditsNextCursorPredictionUrl);
1066+
const secretKey = this.configService.getConfig(ConfigKey.Internal.InlineEditsNextCursorPredictionApiKey);
1067+
1068+
const endpoint = this.instaService.createInstance(ChatEndpoint, {
1069+
id: modelName,
1070+
name: 'nes.nextCursorPosition',
1071+
urlOrRequestMetadata: url,
1072+
model_picker_enabled: false,
1073+
is_chat_default: false,
1074+
is_chat_fallback: false,
1075+
version: '',
1076+
capabilities: {
1077+
type: 'chat',
1078+
family: '',
1079+
tokenizer: TokenizerType.CL100K,
1080+
limits: undefined,
1081+
supports: {
1082+
parallel_tool_calls: false,
1083+
tool_calls: false,
1084+
streaming: true,
1085+
vision: false,
1086+
prediction: false,
1087+
thinking: false
1088+
}
1089+
},
1090+
});
1091+
1092+
const response = await endpoint.makeChatRequest2(
1093+
{
1094+
messages,
1095+
debugName: 'nes.nextCursorPosition',
1096+
finishedCb: undefined,
1097+
location: ChatLocation.Other,
1098+
requestOptions: {
1099+
secretKey,
1100+
}
1101+
},
1102+
CancellationToken.None
1103+
);
1104+
1105+
if (response.type !== ChatFetchResponseType.Success) {
1106+
return;
1107+
}
1108+
1109+
try {
1110+
const trimmed = response.value.trim();
1111+
const lineNumber = parseInt(trimmed, 10);
1112+
if (isNaN(lineNumber) || lineNumber < 0) {
1113+
throw new Error(`parsed line number is NaN or negative: ${trimmed}`);
1114+
}
1115+
1116+
return lineNumber;
1117+
} catch (err) {
1118+
tracer.trace(`Failed to parse predicted line number from response '${response.value}': ${err}`);
1119+
return undefined;
1120+
}
1121+
1122+
}
1123+
9991124
private determinePromptingStrategy(): xtabPromptOptions.PromptingStrategy | undefined {
10001125
const isXtabUnifiedModel = this.configService.getExperimentBasedConfig(ConfigKey.Internal.InlineEditsXtabUseUnifiedModel, this.expService);
10011126
const isCodexV21NesUnified = this.configService.getExperimentBasedConfig(ConfigKey.Internal.InlineEditsXtabCodexV21NesUnified, this.expService);

src/platform/configuration/common/configurationService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,10 @@ export namespace ConfigKey {
664664
export const InlineEditsHideInternalInterface = defineValidatedSetting<boolean>('chat.advanced.inlineEdits.hideInternalInterface', vBoolean(), false, INTERNAL_RESTRICTED);
665665
export const InlineEditsLogCancelledRequests = defineValidatedSetting<boolean>('chat.advanced.inlineEdits.logCancelledRequests', vBoolean(), false, INTERNAL_RESTRICTED);
666666
export const InlineEditsUnification = defineExpSetting<boolean>('chat.advanced.inlineEdits.unification', false, INTERNAL_RESTRICTED);
667+
export const InlineEditsNextCursorPredictionEnabled = defineExpSetting<boolean>('chat.advanced.inlineEdits.nextCursorPrediction.enabled', false, INTERNAL_RESTRICTED);
668+
export const InlineEditsNextCursorPredictionModelName = defineExpSetting<string | undefined>('chat.advanced.inlineEdits.nextCursorPrediction.modelName', undefined, INTERNAL_RESTRICTED);
669+
export const InlineEditsNextCursorPredictionUrl = defineValidatedSetting<string | undefined>('chat.advanced.inlineEdits.nextCursorPrediction.url', vString(), undefined, INTERNAL_RESTRICTED);
670+
export const InlineEditsNextCursorPredictionApiKey = defineValidatedSetting<string | undefined>('chat.advanced.inlineEdits.nextCursorPrediction.apiKey', vString(), undefined, INTERNAL_RESTRICTED);
667671
export const InlineEditsXtabProviderUrl = defineValidatedSetting<string | undefined>('chat.advanced.inlineEdits.xtabProvider.url', vString(), undefined, INTERNAL_RESTRICTED);
668672
export const InlineEditsXtabProviderApiKey = defineValidatedSetting<string | undefined>('chat.advanced.inlineEdits.xtabProvider.apiKey', vString(), undefined, INTERNAL_RESTRICTED);
669673
export const InlineEditsXtabProviderModelConfiguration = defineValidatedSetting<xtabPromptOptions.ModelConfiguration | undefined>('chat.advanced.inlineEdits.xtabProvider.modelConfiguration', xtabPromptOptions.MODEL_CONFIGURATION_VALIDATOR, undefined, INTERNAL_RESTRICTED);

0 commit comments

Comments
 (0)