diff --git a/core/autocomplete/CompletionProvider.ts b/core/autocomplete/CompletionProvider.ts index 377dfa1e513..6deb9d132e1 100644 --- a/core/autocomplete/CompletionProvider.ts +++ b/core/autocomplete/CompletionProvider.ts @@ -215,12 +215,6 @@ export class CompletionProvider { const multiline = !helper.options.transform || shouldCompleteMultiline(helper); - const rawGeneration = await llm.complete( - prompt, - token, - completionOptions, - ); - const completionStream = this.completionStreamer.streamCompletionWithFilters( token, diff --git a/core/config/loadLocalAssistants.ts b/core/config/loadLocalAssistants.ts index b6a29212d33..2a8135dc14a 100644 --- a/core/config/loadLocalAssistants.ts +++ b/core/config/loadLocalAssistants.ts @@ -35,7 +35,11 @@ async function getDefinitionFilesInDir( } const overrideDefaultIgnores = ignore() - .add(DEFAULT_IGNORE_FILETYPES.filter((t) => t !== "config.yaml")) + .add( + DEFAULT_IGNORE_FILETYPES.filter( + (t) => t !== "config.yaml" && t !== "config.yml", + ), + ) .add(DEFAULT_IGNORE_DIRS); const uris = await walkDir(dir, ide, { diff --git a/core/config/loadLocalAssistants.vitest.ts b/core/config/loadLocalAssistants.vitest.ts index 8527eada5db..316687db1d1 100644 --- a/core/config/loadLocalAssistants.vitest.ts +++ b/core/config/loadLocalAssistants.vitest.ts @@ -18,6 +18,8 @@ describe("ASSISTANTS getAllDotContinueDefinitionFiles with fileExtType option", [".continue/assistants/assistant2.yml", "yaml content 2"], [".continue/assistants/assistant3.md", "markdown content 1"], [".continue/assistants/assistant4.txt", "txt content"], + [".continue/assistants/config.yaml", "txt content"], + [".continue/assistants/config.yml", "txt content"], ]); }); @@ -38,9 +40,14 @@ describe("ASSISTANTS getAllDotContinueDefinitionFiles with fileExtType option", options, "assistants", ); - expect(result).toHaveLength(2); + expect(result).toHaveLength(4); expect(result.map((f) => f.path.split("/").pop())).toEqual( - expect.arrayContaining(["assistant1.yaml", "assistant2.yml"]), + expect.arrayContaining([ + "assistant1.yaml", + "assistant2.yml", + "config.yaml", + "config.yml", + ]), ); expect(result.map((f) => f.path.split("/").pop())).not.toContain( "assistant3.md", @@ -69,6 +76,12 @@ describe("ASSISTANTS getAllDotContinueDefinitionFiles with fileExtType option", expect(result.map((f) => f.path.split("/").pop())).not.toContain( "assistant2.yml", ); + expect(result.map((f) => f.path.split("/").pop())).not.toContain( + "config.yml", + ); + expect(result.map((f) => f.path.split("/").pop())).not.toContain( + "config.yaml", + ); }); it("should return all supported files when fileExtType is not specified", async () => { @@ -83,11 +96,13 @@ describe("ASSISTANTS getAllDotContinueDefinitionFiles with fileExtType option", options, "assistants", ); - expect(result).toHaveLength(3); + expect(result).toHaveLength(5); expect(result.map((f) => f.path.split("/").pop())).toEqual( expect.arrayContaining([ "assistant1.yaml", "assistant2.yml", + "config.yml", + "config.yaml", "assistant3.md", ]), ); @@ -124,9 +139,14 @@ describe("ASSISTANTS getAllDotContinueDefinitionFiles with fileExtType option", workspaceOnOptions, "assistants", ); - expect(workspaceResult).toHaveLength(2); + expect(workspaceResult).toHaveLength(4); expect(workspaceResult.map((f) => f.path.split("/").pop())).toEqual( - expect.arrayContaining(["assistant1.yaml", "assistant2.yml"]), + expect.arrayContaining([ + "assistant1.yaml", + "assistant2.yml", + "config.yaml", + "config.yml", + ]), ); }); @@ -187,7 +207,7 @@ describe("ASSISTANTS getAllDotContinueDefinitionFiles with fileExtType option", options, "assistants", ); - expect(result).toHaveLength(2); + expect(result).toHaveLength(4); const yamlFile = result.find((f) => f.path.includes("assistant1.yaml")); expect(yamlFile?.content).toBe("yaml content 1"); }); @@ -212,9 +232,14 @@ describe("ASSISTANTS getAllDotContinueDefinitionFiles with fileExtType option", "assistants", ); // Should only get lowercase extensions (current implementation) - expect(yamlResult).toHaveLength(2); + expect(yamlResult).toHaveLength(4); expect(yamlResult.map((f) => f.path.split("/").pop())).toEqual( - expect.arrayContaining(["assistant1.yaml", "assistant2.yml"]), + expect.arrayContaining([ + "assistant1.yaml", + "assistant2.yml", + "config.yaml", + "config.yml", + ]), ); expect(yamlResult.map((f) => f.path.split("/").pop())).not.toContain( "assistant5.YAML", diff --git a/core/nextEdit/NextEditLoggingService.ts b/core/nextEdit/NextEditLoggingService.ts index 7c448f2d746..4ed9ed4e9bf 100644 --- a/core/nextEdit/NextEditLoggingService.ts +++ b/core/nextEdit/NextEditLoggingService.ts @@ -11,6 +11,16 @@ export class NextEditLoggingService { private _abortControllers = new Map(); private _logRejectionTimeouts = new Map(); private _outcomes = new Map(); + // Track minimal data for completions that get aborted before we have full outcome. + private _pendingCompletions = new Map< + string, + { + startTime: number; + modelName?: string; + modelProvider?: string; + filepath?: string; + } + >(); _lastDisplayedCompletion: { id: string; displayedAt: number } | undefined = undefined; @@ -26,21 +36,54 @@ export class NextEditLoggingService { public createAbortController(completionId: string): AbortController { const abortController = new AbortController(); this._abortControllers.set(completionId, abortController); + this.trackPendingCompletion(completionId); return abortController; } public deleteAbortController(completionId: string) { this._abortControllers.delete(completionId); + this._pendingCompletions.delete(completionId); + } + + // Keep track of a new completion request. + public trackPendingCompletion(completionId: string) { + this._pendingCompletions.set(completionId, { + startTime: Date.now(), + }); + } + + // Update pending completion info as it becomes available. + public updatePendingCompletion( + completionId: string, + data: { + modelName?: string; + modelProvider?: string; + filepath?: string; + }, + ) { + const pending = this._pendingCompletions.get(completionId); + if (pending) { + this._pendingCompletions.set(completionId, { ...pending, ...data }); + } else { + // If we haven't tracked it yet, create new entry with provided data. + this._pendingCompletions.set(completionId, { + startTime: Date.now(), + ...data, + }); + } } public cancel() { - this._abortControllers.forEach((abortController, id) => { + this._abortControllers.forEach((abortController, completionId) => { + this.handleAbort(completionId); abortController.abort(); }); this._abortControllers.clear(); } public accept(completionId: string): NextEditOutcome | undefined { + this._pendingCompletions.delete(completionId); + if (this._logRejectionTimeouts.has(completionId)) { clearTimeout(this._logRejectionTimeouts.get(completionId)); this._logRejectionTimeouts.delete(completionId); @@ -48,6 +91,7 @@ export class NextEditLoggingService { if (this._outcomes.has(completionId)) { const outcome = this._outcomes.get(completionId)!; outcome.accepted = true; + outcome.aborted = false; this.logNextEditOutcome(outcome); this._outcomes.delete(completionId); return outcome; @@ -55,6 +99,8 @@ export class NextEditLoggingService { } public reject(completionId: string): NextEditOutcome | undefined { + this._pendingCompletions.delete(completionId); + if (this._logRejectionTimeouts.has(completionId)) { clearTimeout(this._logRejectionTimeouts.get(completionId)); this._logRejectionTimeouts.delete(completionId); @@ -63,6 +109,7 @@ export class NextEditLoggingService { if (this._outcomes.has(completionId)) { const outcome = this._outcomes.get(completionId)!; outcome.accepted = false; + outcome.aborted = false; this.logNextEditOutcome(outcome); this._outcomes.delete(completionId); return outcome; @@ -86,12 +133,19 @@ export class NextEditLoggingService { } public markDisplayed(completionId: string, outcome: NextEditOutcome) { + // Remove from pending since we now have full data. + this._pendingCompletions.delete(completionId); + outcome.aborted = false; + const logRejectionTimeout = setTimeout(() => { // Wait 10 seconds, then assume it wasn't accepted outcome.accepted = false; + outcome.aborted = false; this.logNextEditOutcome(outcome); this._logRejectionTimeouts.delete(completionId); + this._outcomes.delete(completionId); }, COUNT_COMPLETION_REJECTED_AFTER); + this._outcomes.set(completionId, outcome); this._logRejectionTimeouts.set(completionId, logRejectionTimeout); @@ -124,7 +178,62 @@ export class NextEditLoggingService { }; } + public handleAbort(completionId: string) { + // Clear any pending rejection timeout. + if (this._logRejectionTimeouts.has(completionId)) { + clearTimeout(this._logRejectionTimeouts.get(completionId)); + this._logRejectionTimeouts.delete(completionId); + } + + // If we have the full outcome, log it as aborted. + if (this._outcomes.has(completionId)) { + const outcome = this._outcomes.get(completionId)!; + outcome.accepted = false; + outcome.aborted = true; + this.logNextEditOutcome(outcome); + this._outcomes.delete(completionId); + } else { + // Log minimal abort event for requests that never got displayed. + const pendingData = this._pendingCompletions.get(completionId); + + const minimalAbortOutcome: Partial = { + completionId, + accepted: false, + aborted: true, + timestamp: Date.now(), + uniqueId: completionId, + elapsed: pendingData ? Date.now() - pendingData.startTime : 0, + modelName: pendingData?.modelName || "unknown", + modelProvider: pendingData?.modelProvider || "unknown", + fileUri: pendingData?.filepath || "unknown", + + // Empty/default values for fields we don't have. + completion: "", + prompt: "", + userEdits: "", + userExcerpts: "", + originalEditableRange: "", + workspaceDirUri: "", + cursorPosition: { line: -1, character: -1 }, + finalCursorPosition: { line: -1, character: -1 }, + editableRegionStartLine: -1, + editableRegionEndLine: -1, + diffLines: [], + completionOptions: {}, + }; + + this.logNextEditOutcome(minimalAbortOutcome as NextEditOutcome); + } + + // Clean up. + this._pendingCompletions.delete(completionId); + } + private logNextEditOutcome(outcome: NextEditOutcome) { + if (outcome.aborted === undefined) { + outcome.aborted = false; + } + void DataLogger.getInstance().logDevData({ name: "nextEditOutcome", data: outcome, diff --git a/core/nextEdit/NextEditProvider.ts b/core/nextEdit/NextEditProvider.ts index 5bd199e050e..ebd9f5641a0 100644 --- a/core/nextEdit/NextEditProvider.ts +++ b/core/nextEdit/NextEditProvider.ts @@ -311,7 +311,11 @@ export class NextEditProvider { input.completionId, ); token = controller.signal; + } else { + // Token was provided externally, just track the completion. + this.loggingService.trackPendingCompletion(input.completionId); } + const startTime = Date.now(); const options = await this._getAutocompleteOptions(); @@ -325,6 +329,13 @@ export class NextEditProvider { return { token, startTime, helper: undefined }; } + // Update pending completion with model info. + this.loggingService.updatePendingCompletion(input.completionId, { + modelName: llm.model, + modelProvider: llm.providerName, + filepath: input.filepath, + }); + // Check model capabilities if (!modelSupportsNextEdit(llm.capabilities, llm.model, llm.title)) { console.error(`${llm.model} is not capable of next edit.`); diff --git a/core/nextEdit/providers/BaseNextEditProvider.ts b/core/nextEdit/providers/BaseNextEditProvider.ts index 02d9c3998a9..5b99af811b9 100644 --- a/core/nextEdit/providers/BaseNextEditProvider.ts +++ b/core/nextEdit/providers/BaseNextEditProvider.ts @@ -298,7 +298,7 @@ export abstract class BaseNextEditProvider { return { elapsed: Date.now() - outcomeCtx.startTime, modelProvider: outcomeCtx.llm.underlyingProviderName, - modelName: outcomeCtx.llm.model + ":zetaDataset", + modelName: outcomeCtx.llm.model, completionOptions: null, completionId: outcomeCtx.completionId || outcomeCtx.helper.input.completionId, diff --git a/core/nextEdit/types.ts b/core/nextEdit/types.ts index 78ca343aedc..8d08f8216be 100644 --- a/core/nextEdit/types.ts +++ b/core/nextEdit/types.ts @@ -63,6 +63,7 @@ export interface NextEditOutcome extends TabAutocompleteOptions { cursorPosition: { line: number; character: number }; finalCursorPosition: { line: number; character: number }; accepted?: boolean; + aborted?: boolean; editableRegionStartLine: number; editableRegionEndLine: number; diffLines: DiffLine[]; diff --git a/extensions/vscode/e2e/actions/Global.actions.ts b/extensions/vscode/e2e/actions/Global.actions.ts index eaafbb10e55..e6f8a3bee76 100644 --- a/extensions/vscode/e2e/actions/Global.actions.ts +++ b/extensions/vscode/e2e/actions/Global.actions.ts @@ -1,9 +1,10 @@ import { + By, EditorView, InputBox, TextEditor, VSBrowser, - Workbench, + Workbench } from "vscode-extension-tester"; import { DEFAULT_TIMEOUT } from "../constants"; @@ -82,4 +83,80 @@ export class GlobalActions { console.warn(`Failed to delete file ${filePath}:`, error); } } + + static async setNextEditEnabled(enabled: boolean) { + const workbench = new Workbench(); + + await workbench.openCommandPrompt(); + process.env.CONTINUE_E2E_NON_NEXT_EDIT_TEST = 'true'; + + + // Initial wait and clear + await TestUtils.waitForTimeout(1000); + await GlobalActions.clearAllNotifications(); + + const statusBar = await workbench.getStatusBar(); + + // Robust element finding with text validation + const continueItem = await TestUtils.waitForSuccess( + async () => { + // Clear any new notifications + try { + await GlobalActions.clearAllNotifications(); + } catch (e) { + // Ignore + } + + const element = await statusBar.findElement( + By.xpath("//*[contains(text(), 'Continue')]") + ); + + // Validate we can get text + const text = await element.getText(); + if (!text || text.trim() === '') { + // Try alternative methods + const textContent = await element.getAttribute('textContent'); + if (!textContent || textContent.trim() === '') { + throw new Error('Text not yet available'); + } + } + + return element; + }, + DEFAULT_TIMEOUT.MD + ); + + // Get text with retry + const text = await TestUtils.waitForSuccess( + async () => { + const itemText = await continueItem.getText(); + if (!itemText || itemText.trim() === '') { + // Fallback to textContent + const textContent = await continueItem.getAttribute('textContent'); + if (textContent && textContent.trim() !== '') { + return textContent; + } + throw new Error('Text content not yet available'); + } + return itemText; + }, + DEFAULT_TIMEOUT.MD + ); + + console.log("Final text:", text); + + const hasNE = text.includes('(NE)'); + console.log("hasNE:", hasNE); + + if (hasNE !== enabled) { + await workbench.executeCommand('Continue: Toggle Next Edit'); + // Clear any resulting notifications + await TestUtils.waitForTimeout(500); + await GlobalActions.clearAllNotifications(); + } + } + + static async disableNextEdit() { + await this.setNextEditEnabled(false); + } } diff --git a/extensions/vscode/e2e/tests/Autocomplete.test.ts b/extensions/vscode/e2e/tests/Autocomplete.test.ts index 4123b480436..8ace8ddc349 100644 --- a/extensions/vscode/e2e/tests/Autocomplete.test.ts +++ b/extensions/vscode/e2e/tests/Autocomplete.test.ts @@ -9,7 +9,9 @@ describe("Autocomplete", () => { let editor: TextEditor; before(async function () { + this.timeout(DEFAULT_TIMEOUT.MD); process.env.NEXT_EDIT_TEST_ENABLED = "false"; + await GlobalActions.disableNextEdit(); }); beforeEach(async function () { diff --git a/extensions/vscode/e2e/tests/Edit.test.ts b/extensions/vscode/e2e/tests/Edit.test.ts index 46b1506a240..2c50c89bb8e 100644 --- a/extensions/vscode/e2e/tests/Edit.test.ts +++ b/extensions/vscode/e2e/tests/Edit.test.ts @@ -24,6 +24,7 @@ describe("Edit Test", () => { await GUIActions.moveContinueToSidebar(VSBrowser.instance.driver); await GlobalActions.openTestWorkspace(); ({ editor } = await GlobalActions.createAndOpenNewTextFile()); + await GlobalActions.disableNextEdit(); }); beforeEach(async function () { diff --git a/extensions/vscode/e2e/tests/GUI.test.ts b/extensions/vscode/e2e/tests/GUI.test.ts index f122fdcb273..0760a607eab 100644 --- a/extensions/vscode/e2e/tests/GUI.test.ts +++ b/extensions/vscode/e2e/tests/GUI.test.ts @@ -7,7 +7,7 @@ import { WebDriver, WebElement, WebView, - until, + until } from "vscode-extension-tester"; import { GlobalActions } from "../actions/Global.actions"; @@ -21,11 +21,12 @@ describe("GUI Test", () => { let driver: WebDriver; before(async function () { - this.timeout(DEFAULT_TIMEOUT.XL); + this.timeout(DEFAULT_TIMEOUT.XL + DEFAULT_TIMEOUT.MD + DEFAULT_TIMEOUT.MD); // Uncomment this line for faster testing await GUIActions.moveContinueToSidebar(VSBrowser.instance.driver); await GlobalActions.openTestWorkspace(); await GlobalActions.clearAllNotifications(); + await GlobalActions.disableNextEdit(); }); beforeEach(async function () { diff --git a/extensions/vscode/e2e/tests/KeyboardShortcuts.test.ts b/extensions/vscode/e2e/tests/KeyboardShortcuts.test.ts index e9b79472f0d..5dff86d5e5b 100644 --- a/extensions/vscode/e2e/tests/KeyboardShortcuts.test.ts +++ b/extensions/vscode/e2e/tests/KeyboardShortcuts.test.ts @@ -12,6 +12,7 @@ import { until, } from "vscode-extension-tester"; +import { GlobalActions } from "../actions/Global.actions"; import { GUIActions } from "../actions/GUI.actions"; import { KeyboardShortcutsActions } from "../actions/KeyboardShortcuts.actions"; import { DEFAULT_TIMEOUT } from "../constants"; @@ -26,6 +27,7 @@ describe("Keyboard Shortcuts", () => { before(async function () { this.timeout(DEFAULT_TIMEOUT.XL); await GUIActions.moveContinueToSidebar(VSBrowser.instance.driver); + await GlobalActions.disableNextEdit(); }); beforeEach(async function () { diff --git a/extensions/vscode/e2e/tests/PromptFile.test.ts b/extensions/vscode/e2e/tests/PromptFile.test.ts index 2717fd17eee..7f6393ead76 100644 --- a/extensions/vscode/e2e/tests/PromptFile.test.ts +++ b/extensions/vscode/e2e/tests/PromptFile.test.ts @@ -13,6 +13,11 @@ import { DEFAULT_TIMEOUT } from "../constants"; describe("Prompt file", () => { let editor: TextEditor; + before(async function () { + this.timeout(DEFAULT_TIMEOUT.MD); + await GlobalActions.disableNextEdit(); + }); + beforeEach(async function () { this.timeout(DEFAULT_TIMEOUT.XL); diff --git a/extensions/vscode/package-lock.json b/extensions/vscode/package-lock.json index b4d581f9e3e..d980d68f4f0 100644 --- a/extensions/vscode/package-lock.json +++ b/extensions/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "continue", - "version": "1.1.79", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "continue", - "version": "1.1.79", + "version": "1.3.1", "license": "Apache-2.0", "dependencies": { "@continuedev/config-types": "file:../../packages/config-types", @@ -116,7 +116,6 @@ "@sentry/esbuild-plugin": "^4.0.2", "@sentry/node": "^9.43.0", "@sentry/vite-plugin": "^4.0.2", - "@typescript-eslint/eslint-plugin": "^7.8.0", "@xenova/transformers": "2.14.0", "adf-to-md": "^1.1.0", "async-mutex": "^0.5.0", @@ -135,7 +134,7 @@ "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.3", "iconv-lite": "^0.6.3", - "ignore": "^5.3.1", + "ignore": "^7.0.5", "is-localhost-ip": "^2.0.0", "jinja-js": "0.1.8", "js-tiktoken": "^1.0.8", @@ -196,6 +195,8 @@ "@types/tar": "^6.1.13", "@types/uuid": "^9.0.7", "@types/win-ca": "^3.5.4", + "@typescript-eslint/eslint-plugin": "^8.40.0", + "@typescript-eslint/parser": "^8.40.0", "cross-env": "^7.0.3", "esbuild": "0.17.19", "eslint": "^8", diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index af10a7043f1..3457833819d 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -111,6 +111,11 @@ "default": true, "markdownDescription": "Enable Continue's tab autocomplete feature. Read our walkthrough to learn about configuration and how to share feedback: [continue.dev › Walkthrough: Tab Autocomplete (beta)](https://docs.continue.dev/features/tab-autocomplete)" }, + "continue.enableNextEdit": { + "type": "boolean", + "default": true, + "markdownDescription": "Enable Continue's next edit feature. Read our docs to learn about configuration and how to share feedback: [continue.dev › Features › Autocomplete › Next Edit (experimental)](https://docs.continue.dev/features/autocomplete/next-edit)" + }, "continue.pauseTabAutocompleteOnBattery": { "type": "boolean", "default": false, @@ -335,6 +340,10 @@ "title": "Enter Enterprise License Key", "group": "Continue" }, + { + "command": "continue.toggleNextEditEnabled", + "title": "Continue: Toggle Next Edit" + }, { "command": "continue.nextEditWindow.hideNextEditSuggestion", "category": "Continue", @@ -458,6 +467,12 @@ "mac": "alt+a", "key": "alt+a" }, + { + "command": "continue.toggleNextEditEnabled", + "key": "ctrl+k ctrl+n", + "mac": "cmd+k cmd+n", + "when": "editorTextFocus" + }, { "command": "continue.nextEditWindow.hideNextEditSuggestion", "key": "escape", diff --git a/extensions/vscode/src/activation/InlineTipManager.ts b/extensions/vscode/src/activation/InlineTipManager.ts index 98fa4ed32db..4a544147cc3 100644 --- a/extensions/vscode/src/activation/InlineTipManager.ts +++ b/extensions/vscode/src/activation/InlineTipManager.ts @@ -235,7 +235,7 @@ export class InlineTipManager { } private createSvgTooltipDecoration() { - var backgroundColour = 0; + var backgroundColour = "#333333"; if (this.theme) { backgroundColour = this.theme.colors["editor.background"]; } @@ -258,12 +258,15 @@ export class InlineTipManager { "font-size": SVG_CONFIG.fontSize, }; - if (!this.theme) { - return; - } + // if (!this.theme) { + // return; + // } try { - const svgContent = svgBuilder + const builder = svgBuilder.newInstance + ? svgBuilder.newInstance() + : svgBuilder; + const svgContent = builder .width(SVG_CONFIG.tipWidth) .height(SVG_CONFIG.tipHeight) // Chat @@ -271,7 +274,7 @@ export class InlineTipManager { { ...baseTextConfig, x: SVG_CONFIG.chatLabelX, - fill: this.theme.colors["editor.foreground"], + fill: this.theme?.colors["editor.foreground"] ?? SVG_CONFIG.stroke, }, SVG_CONFIG.chatLabel, ) @@ -288,7 +291,7 @@ export class InlineTipManager { { ...baseTextConfig, x: SVG_CONFIG.editLabelX, - fill: this.theme.colors["editor.foreground"], + fill: this.theme?.colors["editor.foreground"] ?? SVG_CONFIG.stroke, }, SVG_CONFIG.editLabel, ) diff --git a/extensions/vscode/src/activation/JumpManager.ts b/extensions/vscode/src/activation/JumpManager.ts index e77e6a1925d..2474f1e4e57 100644 --- a/extensions/vscode/src/activation/JumpManager.ts +++ b/extensions/vscode/src/activation/JumpManager.ts @@ -130,7 +130,10 @@ export class JumpManager { }; try { - const svgContent = svgBuilder + const builder = svgBuilder.newInstance + ? svgBuilder.newInstance() + : svgBuilder; + const svgContent = builder .width(SVG_CONFIG.getTipWidth()) .height(SVG_CONFIG.getTipHeight()) .text( diff --git a/extensions/vscode/src/autocomplete/completionProvider.ts b/extensions/vscode/src/autocomplete/completionProvider.ts index f081d243057..add7c315bf7 100644 --- a/extensions/vscode/src/autocomplete/completionProvider.ts +++ b/extensions/vscode/src/autocomplete/completionProvider.ts @@ -199,7 +199,18 @@ export class ContinueCompletionProvider try { const abortController = new AbortController(); const signal = abortController.signal; - token.onCancellationRequested(() => abortController.abort()); + const completionId = uuidv4(); + + if (this.isNextEditActive) { + this.nextEditLoggingService.trackPendingCompletion(completionId); + } + + token.onCancellationRequested(() => { + abortController.abort(); + if (this.isNextEditActive) { + this.nextEditLoggingService.handleAbort(completionId); + } + }); // Handle notebook cells let pos = { @@ -252,7 +263,7 @@ export class ContinueCompletionProvider const wasManuallyTriggered = context.triggerKind === vscode.InlineCompletionTriggerKind.Invoke; - const completionId = uuidv4(); + // const completionId = uuidv4(); const filepath = document.uri.toString(); const recentlyVisitedRanges = this.recentlyVisitedRanges.getSnippets(); let recentlyEditedRanges = diff --git a/extensions/vscode/src/autocomplete/statusBar.ts b/extensions/vscode/src/autocomplete/statusBar.ts index 4a26106e7a0..106a4fac258 100644 --- a/extensions/vscode/src/autocomplete/statusBar.ts +++ b/extensions/vscode/src/autocomplete/statusBar.ts @@ -3,6 +3,7 @@ import { EXTENSION_NAME } from "core/control-plane/env"; import * as vscode from "vscode"; import { Battery } from "../util/battery"; +import { getMetaKeyLabel } from "../util/util"; import { CONTINUE_WORKSPACE_KEY, getContinueWorkspaceConfig, @@ -50,18 +51,36 @@ const statusBarItemText = ( return "$(alert) Continue (config error)"; } + let text: string; switch (status) { case undefined: if (loading) { - return "$(loading~spin) Continue"; + text = "$(loading~spin) Continue"; + } else { + text = "Continue"; } + break; case StatusBarStatus.Disabled: - return "$(circle-slash) Continue"; + text = "$(circle-slash) Continue"; + break; case StatusBarStatus.Enabled: - return "$(check) Continue"; + text = "$(check) Continue"; + break; case StatusBarStatus.Paused: - return "$(debug-pause) Continue"; + text = "$(debug-pause) Continue"; + break; + default: + text = "Continue"; + } + + // Append Next Edit indicator if enabled. + const config = vscode.workspace.getConfiguration(EXTENSION_NAME); + const nextEditEnabled = config.get("enableNextEdit") ?? false; + if (nextEditEnabled) { + text += " (NE)"; } + + return text; }; const statusBarItemTooltip = (status: StatusBarStatus | undefined) => { @@ -70,7 +89,11 @@ const statusBarItemTooltip = (status: StatusBarStatus | undefined) => { case StatusBarStatus.Disabled: return "Click to enable tab autocomplete"; case StatusBarStatus.Enabled: - return "Tab autocomplete is enabled"; + const config = vscode.workspace.getConfiguration(EXTENSION_NAME); + const nextEditEnabled = config.get("enableNextEdit") ?? false; + return nextEditEnabled + ? "Next Edit is enabled" + : "Tab autocomplete is enabled"; case StatusBarStatus.Paused: return "Tab autocomplete is paused"; } @@ -199,3 +222,45 @@ export function getAutocompleteStatusBarTitle( return title; } + +const USE_FIM_MENU_ITEM_LABEL = "$(export) Use FIM autocomplete over Next Edit"; +const USE_NEXT_EDIT_MENU_ITEM_LABEL = + "$(sparkle) Use Next Edit over FIM autocomplete"; + +// Shows what items get rendered in the autocomplete menu. +export function getNextEditMenuItems( + currentStatus: StatusBarStatus | undefined, + nextEditEnabled: boolean, +): vscode.QuickPickItem[] { + if (currentStatus !== StatusBarStatus.Enabled) return []; + + return [ + { + label: nextEditEnabled + ? USE_FIM_MENU_ITEM_LABEL + : USE_NEXT_EDIT_MENU_ITEM_LABEL, + description: getMetaKeyLabel() + " + K, " + getMetaKeyLabel() + " + N", + }, + ]; +} + +// Checks if the current selected option is a Next Edit toggle label. +export function isNextEditToggleLabel(label: string): boolean { + return ( + label === USE_FIM_MENU_ITEM_LABEL || label === USE_NEXT_EDIT_MENU_ITEM_LABEL + ); +} + +// Updates the config once Next Edit is toggled. +export function handleNextEditToggle( + label: string, + config: vscode.WorkspaceConfiguration, +) { + const isEnabling = label === USE_NEXT_EDIT_MENU_ITEM_LABEL; + + config.update( + "enableNextEdit", + isEnabling, + vscode.ConfigurationTarget.Global, + ); +} diff --git a/extensions/vscode/src/commands.ts b/extensions/vscode/src/commands.ts index cd75d24bd40..92a30501229 100644 --- a/extensions/vscode/src/commands.ts +++ b/extensions/vscode/src/commands.ts @@ -20,8 +20,11 @@ import { NextEditLoggingService } from "core/nextEdit/NextEditLoggingService"; import { getAutocompleteStatusBarDescription, getAutocompleteStatusBarTitle, + getNextEditMenuItems, getStatusBarStatus, getStatusBarStatusFromQuickPickItemLabel, + handleNextEditToggle, + isNextEditToggleLabel, quickPickStatusText, setupStatusBar, StatusBarStatus, @@ -639,6 +642,8 @@ const getCommandsMap: ( : StatusBarStatus.Disabled; } + const nextEditEnabled = config.get("enableNextEdit") ?? false; + quickPick.items = [ { label: "$(gear) Open settings", @@ -657,6 +662,7 @@ const getCommandsMap: ( description: getMetaKeyLabel() + " + K, " + getMetaKeyLabel() + " + A", }, + ...getNextEditMenuItems(currentStatus, nextEditEnabled), { kind: vscode.QuickPickItemKind.Separator, label: "Switch model", @@ -678,6 +684,8 @@ const getCommandsMap: ( targetStatus === StatusBarStatus.Enabled, vscode.ConfigurationTarget.Global, ); + } else if (isNextEditToggleLabel(selectedOption)) { + handleNextEditToggle(selectedOption, config); } else if ( autocompleteModels.some((model) => model.title === selectedOption) ) { @@ -792,6 +800,30 @@ const getCommandsMap: ( ); } }, + "continue.toggleNextEditEnabled": async () => { + captureCommandTelemetry("toggleNextEditEnabled"); + + const config = vscode.workspace.getConfiguration(EXTENSION_NAME); + const tabAutocompleteEnabled = config.get( + "enableTabAutocomplete", + ); + + if (!tabAutocompleteEnabled) { + vscode.window.showInformationMessage( + "Please enable tab autocomplete first to use Next Edit", + ); + return; + } + + const nextEditEnabled = config.get("enableNextEdit") ?? false; + + // updateNextEditState in VsCodeExtension.ts will handle the validation. + config.update( + "enableNextEdit", + !nextEditEnabled, + vscode.ConfigurationTarget.Global, + ); + }, "continue.forceNextEdit": async () => { captureCommandTelemetry("forceNextEdit"); diff --git a/extensions/vscode/src/diff/vertical/manager.ts b/extensions/vscode/src/diff/vertical/manager.ts index e30f898c0f2..1731b176d3e 100644 --- a/extensions/vscode/src/diff/vertical/manager.ts +++ b/extensions/vscode/src/diff/vertical/manager.ts @@ -141,7 +141,16 @@ export class VerticalDiffManager { this.disableDocumentChangeListener(); - vscode.commands.executeCommand("setContext", "continue.diffVisible", false); + void vscode.commands.executeCommand( + "setContext", + "continue.diffVisible", + false, + ); + + void this.webviewProtocol.request( + "focusContinueInputWithoutClear", + undefined, + ); } async acceptRejectVerticalDiffBlock( diff --git a/extensions/vscode/src/extension/VsCodeExtension.ts b/extensions/vscode/src/extension/VsCodeExtension.ts index 943562e80cc..c6fbc803ddf 100644 --- a/extensions/vscode/src/extension/VsCodeExtension.ts +++ b/extensions/vscode/src/extension/VsCodeExtension.ts @@ -84,6 +84,88 @@ export class VsCodeExtension { private ARBITRARY_TYPING_DELAY = 2000; + private async updateNextEditState( + context: vscode.ExtensionContext, + ): Promise { + const { config: continueConfig } = await this.configHandler.loadConfig(); + const autocompleteModel = continueConfig?.selectedModelByRole.autocomplete; + const vscodeConfig = vscode.workspace.getConfiguration(EXTENSION_NAME); + + const modelSupportsNext = + autocompleteModel && + modelSupportsNextEdit( + autocompleteModel.capabilities, + autocompleteModel.model, + autocompleteModel.title, + ); + + // Use smart defaults. + let nextEditEnabled = vscodeConfig.get("enableNextEdit"); + if (nextEditEnabled === undefined) { + // First time - set smart default. + nextEditEnabled = modelSupportsNext ?? false; + await vscodeConfig.update( + "enableNextEdit", + nextEditEnabled, + vscode.ConfigurationTarget.Global, + ); + } + + // Check if Next Edit is enabled but model doesn't support it. + if ( + nextEditEnabled && + !modelSupportsNext && + !isNextEditTest() && + process.env.CONTINUE_E2E_NON_NEXT_EDIT_TEST === "true" + ) { + vscode.window + .showWarningMessage( + `The current autocomplete model (${autocompleteModel?.title || "unknown"}) does not support Next Edit.`, + "Disable Next Edit", + "Select different model", + ) + .then((selection) => { + if (selection === "Disable Next Edit") { + vscodeConfig.update( + "enableNextEdit", + false, + vscode.ConfigurationTarget.Global, + ); + } else if (selection === "Select different model") { + vscode.commands.executeCommand( + "continue.openTabAutocompleteConfigMenu", + ); + } + }); + } + + const shouldEnableNextEdit = + (modelSupportsNext && nextEditEnabled) || isNextEditTest(); + + if (shouldEnableNextEdit) { + await setupNextEditWindowManager(context); + this.activateNextEdit(); + await NextEditWindowManager.freeTabAndEsc(); + + const jumpManager = JumpManager.getInstance(); + jumpManager.registerSelectionChangeHandler(); + + const ghostTextAcceptanceTracker = + GhostTextAcceptanceTracker.getInstance(); + ghostTextAcceptanceTracker.registerSelectionChangeHandler(); + + const nextEditWindowManager = NextEditWindowManager.getInstance(); + nextEditWindowManager.registerSelectionChangeHandler(); + } else { + NextEditWindowManager.clearInstance(); + this.deactivateNextEdit(); + await NextEditWindowManager.freeTabAndEsc(); + + JumpManager.clearInstance(); + GhostTextAcceptanceTracker.clearInstance(); + } + } + constructor(context: vscode.ExtensionContext) { // Register auth provider this.workOsAuthProvider = new WorkOsAuthProvider(context, this.uriHandler); @@ -238,39 +320,7 @@ export class VsCodeExtension { this.completionProvider.updateUsingFullFileDiff(shouldUseFullFileDiff); selectionManager.updateUsingFullFileDiff(shouldUseFullFileDiff); - const autocompleteModel = newConfig?.selectedModelByRole.autocomplete; - - if ( - (autocompleteModel && - modelSupportsNextEdit( - autocompleteModel.capabilities, - autocompleteModel.model, - autocompleteModel.title, - )) || - isNextEditTest() - ) { - // Set up next edit window manager only for Continue team members - await setupNextEditWindowManager(context); - this.activateNextEdit(); - await NextEditWindowManager.freeTabAndEsc(); - - const jumpManager = JumpManager.getInstance(); - jumpManager.registerSelectionChangeHandler(); - - const ghostTextAcceptanceTracker = - GhostTextAcceptanceTracker.getInstance(); - ghostTextAcceptanceTracker.registerSelectionChangeHandler(); - - const nextEditWindowManager = NextEditWindowManager.getInstance(); - nextEditWindowManager.registerSelectionChangeHandler(); - } else { - NextEditWindowManager.clearInstance(); - this.deactivateNextEdit(); - await NextEditWindowManager.freeTabAndEsc(); - - JumpManager.clearInstance(); - GhostTextAcceptanceTracker.clearInstance(); - } + await this.updateNextEditState(context); if (configLoadInterrupted) { // Show error in status bar @@ -562,6 +612,10 @@ export class VsCodeExtension { if (event.affectsConfiguration(EXTENSION_NAME)) { const settings = await this.ide.getIdeSettings(); void this.core.invoke("config/ideSettingsUpdate", settings); + + if (event.affectsConfiguration(`${EXTENSION_NAME}.enableNextEdit`)) { + await this.updateNextEditState(context); + } } }); } diff --git a/gui/package-lock.json b/gui/package-lock.json index a3dffda4cd5..541fce5f27f 100644 --- a/gui/package-lock.json +++ b/gui/package-lock.json @@ -129,7 +129,6 @@ "@sentry/esbuild-plugin": "^4.0.2", "@sentry/node": "^9.43.0", "@sentry/vite-plugin": "^4.0.2", - "@typescript-eslint/eslint-plugin": "^7.8.0", "@xenova/transformers": "2.14.0", "adf-to-md": "^1.1.0", "async-mutex": "^0.5.0", @@ -209,6 +208,8 @@ "@types/tar": "^6.1.13", "@types/uuid": "^9.0.7", "@types/win-ca": "^3.5.4", + "@typescript-eslint/eslint-plugin": "^8.40.0", + "@typescript-eslint/parser": "^8.40.0", "cross-env": "^7.0.3", "esbuild": "0.17.19", "eslint": "^8", diff --git a/package-lock.json b/package-lock.json index 6981243e333..b4289d4228a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "wt-mcp-flag", + "name": "continue", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/packages/config-yaml/src/schemas/data/nextEditOutcome/index.ts b/packages/config-yaml/src/schemas/data/nextEditOutcome/index.ts index f294b106821..2cc63c91d49 100644 --- a/packages/config-yaml/src/schemas/data/nextEditOutcome/index.ts +++ b/packages/config-yaml/src/schemas/data/nextEditOutcome/index.ts @@ -17,6 +17,7 @@ export const nextEditOutcomeEventAllSchema = baseDevDataAllSchema.extend({ completion: z.string(), cursorPosition: z.object({ line: z.number(), character: z.number() }), accepted: z.boolean().optional(), + aborted: z.boolean().optional(), modelProvider: z.string(), modelName: z.string(), }); diff --git a/packages/config-yaml/src/schemas/data/nextEditOutcome/v0.2.0.ts b/packages/config-yaml/src/schemas/data/nextEditOutcome/v0.2.0.ts index 82e4edbea08..d7258d2dd22 100644 --- a/packages/config-yaml/src/schemas/data/nextEditOutcome/v0.2.0.ts +++ b/packages/config-yaml/src/schemas/data/nextEditOutcome/v0.2.0.ts @@ -26,6 +26,7 @@ export const nextEditOutcomeEventSchema_0_2_0 = completion: true, cursorPosition: true, accepted: true, + aborted: true, modelProvider: true, modelName: true, }); diff --git a/packages/openai-adapters/package-lock.json b/packages/openai-adapters/package-lock.json index 806a98f2acb..046b1dc37f9 100644 --- a/packages/openai-adapters/package-lock.json +++ b/packages/openai-adapters/package-lock.json @@ -12528,7 +12528,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver"