diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts deleted file mode 100644 index adf28e36..00000000 --- a/packages/core/src/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const DEFAULT_COPILOT_TEMPERATURE = 0.1 as const; -export const DEFAULT_COPILOT_MAX_TOKENS = 500 as const; diff --git a/packages/core/src/copilot.ts b/packages/core/src/copilot.ts index 591c195b..c6a6af19 100644 --- a/packages/core/src/copilot.ts +++ b/packages/core/src/copilot.ts @@ -19,6 +19,7 @@ import type { Provider, } from './types/llm'; import {BaseCopilotMetadata} from './types/metadata'; +import {fetchWithTimeout} from './utils/fetch-with-timeout'; import validate from './validator'; export abstract class Copilot { @@ -139,7 +140,7 @@ export abstract class Copilot { requestBody: CompletionCreateParams, headers: Record, ) { - const response = await fetch(endpoint, { + const response = await fetchWithTimeout(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/packages/core/src/defaults.ts b/packages/core/src/defaults.ts new file mode 100644 index 00000000..9ac35b90 --- /dev/null +++ b/packages/core/src/defaults.ts @@ -0,0 +1,2 @@ +export const DEFAULT_COPILOT_MAX_TOKENS = 256 as const; +export const DEFAULT_COPILOT_STOP_SEQUENCE = '\n\n' as const; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 62255bc3..7997b1eb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,7 +1,7 @@ export {Copilot} from './copilot'; export * from './logger'; -export * from './constants'; +export * from './defaults'; export * from './llm/base'; export type * from './types/llm'; diff --git a/packages/core/src/llm/providers/mistral.ts b/packages/core/src/llm/providers/mistral.ts index 0cb3aaca..898ddbfd 100644 --- a/packages/core/src/llm/providers/mistral.ts +++ b/packages/core/src/llm/providers/mistral.ts @@ -1,4 +1,7 @@ -import {DEFAULT_COPILOT_TEMPERATURE} from '../../constants'; +import { + DEFAULT_COPILOT_MAX_TOKENS, + DEFAULT_COPILOT_STOP_SEQUENCE, +} from '../../defaults'; import type {PromptData} from '../../types/copilot'; import type { PickCompletion, @@ -21,10 +24,12 @@ export class MistralHandler extends BaseProviderHandler<'mistral'> { ): PickCompletionCreateParams<'mistral'> { return { model: MODEL_IDS[model], - temperature: DEFAULT_COPILOT_TEMPERATURE, - prompt: `${prompt.context}\n${prompt.instruction}\n\n${metadata.textBeforeCursor}`, + prompt: `${prompt.context}\n${prompt.instruction}\n${metadata.textBeforeCursor}`, + max_tokens: DEFAULT_COPILOT_MAX_TOKENS, + stop: DEFAULT_COPILOT_STOP_SEQUENCE, suffix: metadata.textAfterCursor, stream: false, + top_p: 0.9, }; } diff --git a/packages/core/src/types/llm.ts b/packages/core/src/types/llm.ts index e9289a76..03bad363 100644 --- a/packages/core/src/types/llm.ts +++ b/packages/core/src/types/llm.ts @@ -1,6 +1,6 @@ import { FIMCompletionResponse as MistralFIMCompletion, - FIMCompletionRequest as MistralFIMCompletionCreateParams, + FIMCompletionRequest$Outbound as MistralFIMCompletionCreateParams, } from '@mistralai/mistralai/models/components'; import type {PromptData} from './copilot'; diff --git a/packages/core/src/utils/fetch-with-timeout.ts b/packages/core/src/utils/fetch-with-timeout.ts new file mode 100644 index 00000000..9b3d08cd --- /dev/null +++ b/packages/core/src/utils/fetch-with-timeout.ts @@ -0,0 +1,28 @@ +export const fetchWithTimeout = async ( + url: string, + options: RequestInit = {}, + timeoutMs: number = 5000, +): Promise => { + const controller = new AbortController(); + const {signal} = controller; + + const timeoutId = setTimeout(() => { + controller.abort(); + }, timeoutMs); + + try { + const response = await fetch(url, { + ...options, + signal, + }); + + return response; + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + throw new Error(`Request timed out after ${timeoutMs}ms`); + } + throw error; + } finally { + clearTimeout(timeoutId); + } +}; diff --git a/packages/monacopilot/src/classes/formatter.ts b/packages/monacopilot/src/classes/formatter.ts index 52bde207..a0cf2f7a 100644 --- a/packages/monacopilot/src/classes/formatter.ts +++ b/packages/monacopilot/src/classes/formatter.ts @@ -1,16 +1,8 @@ export class CompletionFormatter { private formattedCompletion = ''; - private currentColumn = 0; - private textBeforeCursorInLine = ''; - constructor( - completion: string, - currentColumn: number, - textBeforeCursorInLine: string, - ) { + constructor(completion: string) { this.formattedCompletion = completion; - this.currentColumn = currentColumn; - this.textBeforeCursorInLine = textBeforeCursorInLine; } public setCompletion(completion: string): CompletionFormatter { @@ -30,41 +22,6 @@ export class CompletionFormatter { return this; } - public indentByColumn(): CompletionFormatter { - const lines = this.formattedCompletion.split('\n'); - - if (this.textBeforeCursorInLine.trim() !== '') { - return this; - } - - const firstLine = lines[0].trimStart(); - const firstLineIndent = lines[0].length - firstLine.length; - - if (lines.length === 1) { - this.formattedCompletion = firstLine; - return this; - } - - this.formattedCompletion = - firstLine + - '\n' + - lines - .slice(1) - .map(line => { - const currentLineIndent = - line.length - line.trimStart().length; - const trimAmount = Math.min( - firstLineIndent, - currentLineIndent, - ); - const trimmedStart = line.slice(trimAmount); - return ' '.repeat(this.currentColumn - 1) + trimmedStart; - }) - .join('\n'); - - return this; - } - private removeMarkdownCodeBlocks(text: string): string { const lines = text.split('\n'); const result: string[] = []; diff --git a/packages/monacopilot/src/processor.ts b/packages/monacopilot/src/processor.ts index bc693950..d84cc2c6 100644 --- a/packages/monacopilot/src/processor.ts +++ b/packages/monacopilot/src/processor.ts @@ -21,7 +21,7 @@ import { } from './types'; import type {EditorInlineCompletionsResult} from './types/monaco'; import {asyncDebounce} from './utils/async-debounce'; -import {getTextBeforeCursor, getTextBeforeCursorInLine} from './utils/editor'; +import {getTextBeforeCursor} from './utils/editor'; import {isCancellationError} from './utils/error'; import {createInlineCompletionResult} from './utils/result'; @@ -86,15 +86,10 @@ export const processInlineCompletions = async ({ }); if (completion) { - const formattedCompletion = new CompletionFormatter( - completion, - pos.column, - getTextBeforeCursorInLine(pos, mdl), - ) + const formattedCompletion = new CompletionFormatter(completion) .removeMarkdownCodeSyntax() .removeExcessiveNewlines() .removeInvalidLineBreaks() - .indentByColumn() .build(); const completionRange = new CompletionRange(monaco); diff --git a/packages/monacopilot/tests/formatter.test.ts b/packages/monacopilot/tests/formatter.test.ts index a49a00cf..9a476c51 100644 --- a/packages/monacopilot/tests/formatter.test.ts +++ b/packages/monacopilot/tests/formatter.test.ts @@ -1,15 +1,12 @@ import {describe, expect, it} from 'vitest'; import {CompletionFormatter} from '../src/classes/formatter'; -import {MOCK_COMPLETION_POS} from './mock'; describe('CompletionFormatter', () => { describe('create', () => { it('should create a new instance of CompletionFormatter', () => { const formatter = new CompletionFormatter( 'const greeting = "Hello, World!";', - MOCK_COMPLETION_POS.column, - '', ); expect(formatter).toBeInstanceOf(CompletionFormatter); }); @@ -19,8 +16,6 @@ describe('CompletionFormatter', () => { it('should remove trailing line breaks', () => { const formatter = new CompletionFormatter( 'function sum(a, b) {\n return a + b;\n}\n\n', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.removeInvalidLineBreaks().build(); expect(result).toBe('function sum(a, b) {\n return a + b;\n}'); @@ -29,8 +24,6 @@ describe('CompletionFormatter', () => { it('should not remove line breaks in the middle of the text', () => { const formatter = new CompletionFormatter( 'const x = 5;\nconst y = 10;\nconst sum = x + y;', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.removeInvalidLineBreaks().build(); expect(result).toBe( @@ -39,11 +32,7 @@ describe('CompletionFormatter', () => { }); it('should handle empty string', () => { - const formatter = new CompletionFormatter( - '', - MOCK_COMPLETION_POS.column, - '', - ); + const formatter = new CompletionFormatter(''); const result = formatter.removeInvalidLineBreaks().build(); expect(result).toBe(''); }); @@ -53,8 +42,6 @@ describe('CompletionFormatter', () => { it('should remove markdown code block syntax', () => { const formatter = new CompletionFormatter( '```\nconst array = [1, 2, 3];\narray.map(x => x * 2);\n```', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.removeMarkdownCodeSyntax().build(); expect(result).toBe( @@ -65,8 +52,6 @@ describe('CompletionFormatter', () => { it('should remove markdown code block in the middle of the text', () => { const formatter = new CompletionFormatter( 'calculate the factorial of a given number using recursion\n```javascript\nfunction calculateFactorial(n) {\n // Base case: factorial of 0 or 1 is 1\n if (n === 0 || n === 1) {\n return 1;\n ', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.removeMarkdownCodeSyntax().build(); expect(result).toBe( @@ -77,8 +62,6 @@ describe('CompletionFormatter', () => { it('should remove multiple markdown code blocks', () => { const formatter = new CompletionFormatter( '```\nfunction greet(name) {\n return `Hello, ${name}!`;\n}\n```\nSome text\n```\nconst result = greet("Alice");\nconsole.log(result);\n```', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.removeMarkdownCodeSyntax().build(); expect(result).toBe( @@ -89,8 +72,6 @@ describe('CompletionFormatter', () => { it('should handle code blocks with language specifiers', () => { const formatter = new CompletionFormatter( '```javascript\nclass Person {\n constructor(name) {\n this.name = name;\n }\n}\n```', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.removeMarkdownCodeSyntax().build(); expect(result).toBe( @@ -99,21 +80,13 @@ describe('CompletionFormatter', () => { }); it('should not modify text without code blocks', () => { - const formatter = new CompletionFormatter( - 'const PI = 3.14159;', - MOCK_COMPLETION_POS.column, - '', - ); + const formatter = new CompletionFormatter('const PI = 3.14159;'); const result = formatter.removeMarkdownCodeSyntax().build(); expect(result).toBe('const PI = 3.14159;'); }); it('should handle empty string', () => { - const formatter = new CompletionFormatter( - '', - MOCK_COMPLETION_POS.column, - '', - ); + const formatter = new CompletionFormatter(''); const result = formatter.removeMarkdownCodeSyntax().build(); expect(result).toBe(''); }); @@ -121,29 +94,19 @@ describe('CompletionFormatter', () => { it('should handle incomplete code blocks', () => { const formatter = new CompletionFormatter( '```\nconst incomplete = true;', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.removeMarkdownCodeSyntax().build(); expect(result).toBe('const incomplete = true;'); }); it('should keep starting whitespaces', () => { - const formatter = new CompletionFormatter( - ' 3.14159;', - MOCK_COMPLETION_POS.column, - '', - ); + const formatter = new CompletionFormatter(' 3.14159;'); const result = formatter.removeMarkdownCodeSyntax().build(); expect(result).toBe(' 3.14159;'); }); it('should keep leading newlines', () => { - const formatter = new CompletionFormatter( - '\nconst PI = 3.14159;', - MOCK_COMPLETION_POS.column, - '', - ); + const formatter = new CompletionFormatter('\nconst PI = 3.14159;'); const result = formatter.removeMarkdownCodeSyntax().build(); expect(result).toBe('\nconst PI = 3.14159;'); }); @@ -151,8 +114,6 @@ describe('CompletionFormatter', () => { it('should preserve leading newline with code block', () => { const formatter = new CompletionFormatter( '\n```\nconst x = 5;\n```', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.removeMarkdownCodeSyntax().build(); expect(result).toBe('\nconst x = 5;'); @@ -163,8 +124,6 @@ describe('CompletionFormatter', () => { it('should replace three or more consecutive newlines with two newlines', () => { const formatter = new CompletionFormatter( 'import React from "react";\n\n\n\nconst App = () => {\n return
Hello React
;\n};', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.removeExcessiveNewlines().build(); expect(result).toBe( @@ -175,8 +134,6 @@ describe('CompletionFormatter', () => { it('should not modify text with two or fewer consecutive newlines', () => { const formatter = new CompletionFormatter( 'const x = 10;\n\nconst y = 20;', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.removeExcessiveNewlines().build(); expect(result).toBe('const x = 10;\n\nconst y = 20;'); @@ -185,8 +142,6 @@ describe('CompletionFormatter', () => { it('should handle multiple occurrences of excessive newlines', () => { const formatter = new CompletionFormatter( 'function add(a, b) {\n return a + b;\n}\n\n\nfunction subtract(a, b) {\n return a - b;\n}\n\n\n\nconst result = add(5, 3);', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.removeExcessiveNewlines().build(); expect(result).toBe( @@ -195,21 +150,13 @@ describe('CompletionFormatter', () => { }); it('should handle empty string', () => { - const formatter = new CompletionFormatter( - '', - MOCK_COMPLETION_POS.column, - '', - ); + const formatter = new CompletionFormatter(''); const result = formatter.removeExcessiveNewlines().build(); expect(result).toBe(''); }); it('should handle string with only newlines', () => { - const formatter = new CompletionFormatter( - '\n\n\n\n', - MOCK_COMPLETION_POS.column, - '', - ); + const formatter = new CompletionFormatter('\n\n\n\n'); const result = formatter.removeExcessiveNewlines().build(); expect(result).toBe('\n\n'); }); @@ -217,132 +164,16 @@ describe('CompletionFormatter', () => { it('should maintain single leading newline', () => { const formatter = new CompletionFormatter( '\nconst x = 10;\n\nconst y = 20;', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.removeExcessiveNewlines().build(); expect(result).toBe('\nconst x = 10;\n\nconst y = 20;'); }); }); - describe('indentByColumn', () => { - it('should not modify a single-line completion', () => { - const input = 'Hello world'; - const formatter = new CompletionFormatter(input, 5, ''); - formatter.indentByColumn(); - expect(formatter.build()).toBe('Hello world'); - }); - - it('should not modify multi-line completions if textBeforeCursorInLine is non-empty', () => { - const input = 'First line\nSecond line'; - const formatter = new CompletionFormatter(input, 5, 'non-empty'); - formatter.indentByColumn(); - expect(formatter.build()).toBe(input); - }); - - it('should correctly indent multi-line completion when textBeforeCursorInLine is empty', () => { - const input = - ' function test() {' + - '\n console.log("hello");' + - '\n }'; - const formatter = new CompletionFormatter(input, 5, ''); - formatter.indentByColumn(); - const expected = - 'function test() {' + - '\n' + - ' ' + - ' console.log("hello");' + - '\n' + - ' ' + - '}'; - expect(formatter.build()).toBe(expected); - }); - - it('should handle case where first line indent is larger than subsequent line indent', () => { - const input = ' First line' + '\n' + ' Second line'; - const formatter = new CompletionFormatter(input, 3, ''); - formatter.indentByColumn(); - const expected = 'First line' + '\n' + ' ' + 'Second line'; - expect(formatter.build()).toBe(expected); - }); - - it('should handle case where first line indent is smaller than subsequent line indent', () => { - const input = ' First line' + '\n' + ' Second line'; - const formatter = new CompletionFormatter(input, 4, ''); - formatter.indentByColumn(); - const expected = 'First line' + '\n' + ' ' + ' Second line'; - expect(formatter.build()).toBe(expected); - }); - - it('should work correctly with empty subsequent lines', () => { - const input = ' First line' + '\n ' + '\n Third line'; - const formatter = new CompletionFormatter(input, 6, ''); - formatter.indentByColumn(); - const expected = - 'First line' + - '\n' + - ' ' + - '' + - '\n' + - ' ' + - 'Third line'; - expect(formatter.build()).toBe(expected); - }); - - it('should work correctly when currentColumn is 1 (no extra prefix added)', () => { - const input = ' Test' + '\n' + ' Line'; - const formatter = new CompletionFormatter(input, 1, ''); - formatter.indentByColumn(); - const expected = 'Test' + '\n' + ' Line'; - expect(formatter.build()).toBe(expected); - }); - - it('should preserve newlines while adjusting indentations', () => { - const input = 'First line\nSecond line\nThird line'; - const formatter = new CompletionFormatter(input, 4, ''); - formatter.indentByColumn(); - const expected = - 'First line' + - '\n' + - ' ' + - 'Second line' + - '\n' + - ' ' + - 'Third line'; - expect(formatter.build()).toBe(expected); - }); - - it('should handle when the first line is only whitespace', () => { - const input = ' ' + '\n' + ' Text after blank first line'; - const formatter = new CompletionFormatter(input, 3, ''); - formatter.indentByColumn(); - const expected = - '' + '\n' + ' ' + ' Text after blank first line'; - expect(formatter.build()).toBe(expected); - }); - - it('should not alter an empty string', () => { - const input = ''; - const formatter = new CompletionFormatter(input, 4, ''); - formatter.indentByColumn(); - expect(formatter.build()).toBe(''); - }); - - it('should treat textBeforeCursorInLine as empty when it contains only whitespace', () => { - const input = ' Alpha' + '\n' + ' Beta'; - const formatter = new CompletionFormatter(input, 5, ' '); - formatter.indentByColumn(); - const expected = 'Alpha' + '\n' + ' ' + ' Beta'; - expect(formatter.build()).toBe(expected); - }); - }); - describe('build', () => { it('should return the formatted completion', () => { const formatter = new CompletionFormatter( ' const square = (x) => x * x; ', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter.build(); expect(result).toBe(' const square = (x) => x * x; '); @@ -353,8 +184,6 @@ describe('CompletionFormatter', () => { it('should allow chaining of multiple formatting methods', () => { const formatter = new CompletionFormatter( '```\nconst fruits = ["apple", "banana", "orange"];\nconst upperFruits = fruits.map(fruit => fruit.toUpperCase());\n```\n\n\n\nconsole.log(upperFruits);', - MOCK_COMPLETION_POS.column, - '', ); const result = formatter .removeMarkdownCodeSyntax() @@ -367,11 +196,7 @@ describe('CompletionFormatter', () => { }); it('should handle empty string with all formatting methods', () => { - const formatter = new CompletionFormatter( - '', - MOCK_COMPLETION_POS.column, - '', - ); + const formatter = new CompletionFormatter(''); const result = formatter .removeMarkdownCodeSyntax() .removeExcessiveNewlines() diff --git a/playground/components/editor.tsx b/playground/components/editor.tsx index 1986abe9..7feecfea 100644 --- a/playground/components/editor.tsx +++ b/playground/components/editor.tsx @@ -26,6 +26,7 @@ const Editor = () => { completionRef.current = registerCompletion(monaco, editor, { endpoint: '/api/code-completion', language: 'python', + trigger: 'onTyping', }); }} />