diff --git a/packages/ai-code-completion/src/browser/ai-code-completion-frontend-module.ts b/packages/ai-code-completion/src/browser/ai-code-completion-frontend-module.ts index 449a37f15ec52..5b72e254bf8cd 100644 --- a/packages/ai-code-completion/src/browser/ai-code-completion-frontend-module.ts +++ b/packages/ai-code-completion/src/browser/ai-code-completion-frontend-module.ts @@ -16,7 +16,7 @@ import { ILogger } from '@theia/core'; import { ContainerModule } from '@theia/core/shared/inversify'; -import { CodeCompletionAgent, CodeCompletionAgentImpl } from '../common/code-completion-agent'; +import { CodeCompletionAgent, CodeCompletionAgentImpl } from './code-completion-agent'; import { AIFrontendApplicationContribution } from './ai-code-frontend-application-contribution'; import { FrontendApplicationContribution, KeybindingContribution, PreferenceContribution } from '@theia/core/lib/browser'; import { Agent } from '@theia/ai-core'; diff --git a/packages/ai-code-completion/src/browser/ai-code-completion-preference.ts b/packages/ai-code-completion/src/browser/ai-code-completion-preference.ts index 01edd996a5243..f9f41c0767f4b 100644 --- a/packages/ai-code-completion/src/browser/ai-code-completion-preference.ts +++ b/packages/ai-code-completion/src/browser/ai-code-completion-preference.ts @@ -20,6 +20,7 @@ import { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/browser/ai-core-pr export const PREF_AI_INLINE_COMPLETION_ENABLE = 'ai-features.codeCompletion.enableCodeCompletion'; export const PREF_AI_INLINE_COMPLETION_AUTOMATIC_ENABLE = 'ai-features.codeCompletion.automaticCodeCompletion'; export const PREF_AI_INLINE_COMPLETION_EXCLUDED_EXTENSIONS = 'ai-features.codeCompletion.excludedFileExtensions'; +export const PREF_AI_INLINE_COMPLETION_STRIP_BACKTICKS = 'ai-features.codeCompletion.stripBackticks'; export const AICodeCompletionPreferencesSchema: PreferenceSchema = { type: 'object', @@ -40,6 +41,14 @@ export const AICodeCompletionPreferencesSchema: PreferenceSchema = { type: 'string' }, default: [] + }, + [PREF_AI_INLINE_COMPLETION_STRIP_BACKTICKS]: { + title: 'Strip Backticks from Inline Completions', + type: 'boolean', + description: 'Remove surrounding backticks from the code returned by some LLMs. If a backtick is detected, all content after the closing\ + backtick is stripped as well. This setting helps ensure plain code is returned when language models use markdown-like formatting.', + default: true } + } }; diff --git a/packages/ai-code-completion/src/browser/ai-code-inline-completion-provider.ts b/packages/ai-code-completion/src/browser/ai-code-inline-completion-provider.ts index b2bb35af85533..48c0c6c959632 100644 --- a/packages/ai-code-completion/src/browser/ai-code-inline-completion-provider.ts +++ b/packages/ai-code-completion/src/browser/ai-code-inline-completion-provider.ts @@ -17,7 +17,7 @@ import * as monaco from '@theia/monaco-editor-core'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { CodeCompletionAgent } from '../common/code-completion-agent'; +import { CodeCompletionAgent } from './code-completion-agent'; import { AgentService } from '@theia/ai-core'; @injectable() diff --git a/packages/ai-code-completion/src/browser/code-completion-agent.spec.ts b/packages/ai-code-completion/src/browser/code-completion-agent.spec.ts new file mode 100644 index 0000000000000..d0ab47f1d351c --- /dev/null +++ b/packages/ai-code-completion/src/browser/code-completion-agent.spec.ts @@ -0,0 +1,67 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { expect } from 'chai'; +import { CodeCompletionAgentImpl } from './code-completion-agent'; + +describe('CodeCompletionAgentImpl', () => { + class TestableCodeCompletionAgent extends CodeCompletionAgentImpl { + public stripBackticksForTest(text: string): string { + return this.stripBackticks(text); + } + } + const agent = new TestableCodeCompletionAgent(); + + describe('stripBackticks', () => { + + it('should remove surrounding backticks and language (TypeScript)', () => { + const input = '```TypeScript\nconsole.log(\"Hello, World!\");```'; + const output = agent.stripBackticksForTest(input); + expect(output).to.equal('console.log("Hello, World!");'); + }); + + it('should remove surrounding backticks and language (md)', () => { + const input = '```md\nconsole.log(\"Hello, World!\");```'; + const output = agent.stripBackticksForTest(input); + expect(output).to.equal('console.log("Hello, World!");'); + }); + + it('should remove all text after second occurrence of backticks', () => { + const input = '```js\nlet x = 10;\n```\nTrailing text should be removed```'; + const output = agent.stripBackticksForTest(input); + expect(output).to.equal('let x = 10;'); + }); + + it('should return the text unchanged if no surrounding backticks', () => { + const input = 'console.log(\"Hello, World!\");'; + const output = agent.stripBackticksForTest(input); + expect(output).to.equal('console.log("Hello, World!");'); + }); + + it('should remove surrounding backticks without language', () => { + const input = '```\nconsole.log(\"Hello, World!\");```'; + const output = agent.stripBackticksForTest(input); + expect(output).to.equal('console.log("Hello, World!");'); + }); + + it('should handle text starting with backticks but no second delimiter', () => { + const input = '```python\nprint(\"Hello, World!\")'; + const output = agent.stripBackticksForTest(input); + expect(output).to.equal('print("Hello, World!")'); + }); + + }); +}); diff --git a/packages/ai-code-completion/src/common/code-completion-agent.ts b/packages/ai-code-completion/src/browser/code-completion-agent.ts similarity index 87% rename from packages/ai-code-completion/src/common/code-completion-agent.ts rename to packages/ai-code-completion/src/browser/code-completion-agent.ts index 1fa9ceaea384a..dc148c9af87bd 100644 --- a/packages/ai-code-completion/src/common/code-completion-agent.ts +++ b/packages/ai-code-completion/src/browser/code-completion-agent.ts @@ -21,6 +21,8 @@ import { import { generateUuid, ILogger } from '@theia/core'; import { inject, injectable, named } from '@theia/core/shared/inversify'; import * as monaco from '@theia/monaco-editor-core'; +import { PREF_AI_INLINE_COMPLETION_STRIP_BACKTICKS } from './ai-code-completion-preference'; +import { PreferenceService } from '@theia/core/lib/browser'; export const CodeCompletionAgent = Symbol('CodeCompletionAgent'); export interface CodeCompletionAgent extends Agent { @@ -30,6 +32,10 @@ export interface CodeCompletionAgent extends Agent { @injectable() export class CodeCompletionAgentImpl implements CodeCompletionAgent { + + @inject(PreferenceService) + protected readonly preferenceService: PreferenceService; + async provideInlineCompletions( model: monaco.editor.ITextModel, position: monaco.Position, @@ -97,10 +103,12 @@ export class CodeCompletionAgentImpl implements CodeCompletionAgent { if (token.isCancellationRequested) { return undefined; } - const completionText = await getTextOfResponse(response); + let completionText = await getTextOfResponse(response); + if (token.isCancellationRequested) { return undefined; } + this.recordingService.recordResponse({ agentId: this.id, sessionId, @@ -108,12 +116,26 @@ export class CodeCompletionAgentImpl implements CodeCompletionAgent { response: completionText, }); + if (this.preferenceService.get(PREF_AI_INLINE_COMPLETION_STRIP_BACKTICKS, true)) { + completionText = this.stripBackticks(completionText); + } + return { items: [{ insertText: completionText }], enableForwardStability: true, }; } + protected stripBackticks(text: string): string { + if (text.startsWith('```')) { + // Remove the first backticks and any language identifier + const startRemoved = text.slice(3).replace(/^\w*\n/, ''); + const secondBacktickIndex = startRemoved.indexOf('```'); + return secondBacktickIndex !== -1 ? startRemoved.slice(0, secondBacktickIndex).trim() : startRemoved.trim(); + } + return text; + } + @inject(ILogger) @named('code-completion-agent') protected logger: ILogger;