diff --git a/public/index.html b/public/index.html
index ba627094c8..be1e7dda98 100644
--- a/public/index.html
+++ b/public/index.html
@@ -25,6 +25,7 @@
window.web_version = !'%REACT_APP_BACKEND%';
window.custom_backend = '%NODE_ENV%' === 'development' && '%REACT_APP_BACKEND%';
window.meta_backend = '%REACT_APP_META_BACKEND%'
+ window.code_assistant_backend = '%REACT_APP_CODE_ASSISTANT_BACKEND%'
diff --git a/src/components/CodeAssistantTelemetry/CodeAssistantTelemetry.tsx b/src/components/CodeAssistantTelemetry/CodeAssistantTelemetry.tsx
new file mode 100644
index 0000000000..e9cd2a45b5
--- /dev/null
+++ b/src/components/CodeAssistantTelemetry/CodeAssistantTelemetry.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import {useSendOpenTabsMutation} from '../../store/reducers/codeAssist';
+import {selectQueriesHistory} from '../../store/reducers/query/query';
+import {useTypedSelector} from '../../utils/hooks';
+
+export function CodeAssistantTelemetry() {
+ const [sendOpenTabs] = useSendOpenTabsMutation();
+ const historyQueries = useTypedSelector(selectQueriesHistory);
+
+ React.useEffect(() => {
+ if (!historyQueries?.length) {
+ return;
+ }
+
+ const tabs = historyQueries.map((query) => ({
+ FileName: `query_${query.queryId}.yql`,
+ Text: query.queryText,
+ }));
+
+ sendOpenTabs(tabs);
+ }, [historyQueries, sendOpenTabs]);
+
+ return null;
+}
diff --git a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx
index 29cd198ad0..4340005d16 100644
--- a/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx
+++ b/src/containers/Tenant/Query/QueryEditor/QueryEditor.tsx
@@ -6,8 +6,10 @@ import throttle from 'lodash/throttle';
import type Monaco from 'monaco-editor';
import {v4 as uuidv4} from 'uuid';
+import {CodeAssistantTelemetry} from '../../../../components/CodeAssistantTelemetry/CodeAssistantTelemetry';
import {MonacoEditor} from '../../../../components/MonacoEditor/MonacoEditor';
import SplitPane from '../../../../components/SplitPane';
+import {registerCompletionCommands} from '../../../../services/codeCompletion';
import {useTracingLevelOptionAvailable} from '../../../../store/reducers/capabilities/hooks';
import {
goToNextQuery,
@@ -42,6 +44,7 @@ import {
import {useChangedQuerySettings} from '../../../../utils/hooks/useChangedQuerySettings';
import {useLastQueryExecutionSettings} from '../../../../utils/hooks/useLastQueryExecutionSettings';
import {YQL_LANGUAGE_ID} from '../../../../utils/monaco/constats';
+import {getCompletionProvider} from '../../../../utils/monaco/yql/ydb.inlineCompletionProvider';
import {QUERY_ACTIONS} from '../../../../utils/query';
import type {InitialPaneState} from '../../utils/paneVisibilityToggleHelpers';
import {
@@ -215,6 +218,14 @@ export default function QueryEditor(props: QueryEditorProps) {
contribution.insert(input);
}
});
+
+ if (window.api.codeAssistant) {
+ const provider = getCompletionProvider();
+ if (provider) {
+ registerCompletionCommands(monaco, provider, editor);
+ }
+ }
+
initResizeHandler(editor);
initUserPrompt(editor, getLastQueryText);
editor.focus();
@@ -323,6 +334,7 @@ export default function QueryEditor(props: QueryEditorProps) {
return (
+ {window.api.codeAssistant && }
();
React.useEffect(() => {
if (previousTenant.current !== tenantName) {
- const register = async () => {
+ const registerSuggestCompletion = async () => {
const {registerYQLCompletionItemProvider} = await import(
'../../utils/monaco/yql/yql.completionItemProvider'
);
registerYQLCompletionItemProvider(tenantName);
};
- register().catch(console.error);
+
+ const registerInlineCompletion = async () => {
+ const {registerInlineCompletionProvider} = await import(
+ '../../utils/monaco/yql/ydb.inlineCompletionProvider'
+ );
+ if (window.api.codeAssistant) {
+ registerInlineCompletionProvider(window.api.codeAssistant);
+ }
+ };
+
+ registerSuggestCompletion().catch(console.error);
+ registerInlineCompletion().catch(console.error);
previousTenant.current = tenantName;
}
}, [tenantName]);
diff --git a/src/services/api/codeAssistant.ts b/src/services/api/codeAssistant.ts
new file mode 100644
index 0000000000..6858ca5ea4
--- /dev/null
+++ b/src/services/api/codeAssistant.ts
@@ -0,0 +1,52 @@
+import {codeAssistantBackend as CODE_ASSISTANT_BACKEND} from '../../store';
+import type {PromptFile, Suggestions, TelemetryEvent, TelemetryOpenTabs} from '../codeCompletion';
+
+import {BaseYdbAPI} from './base';
+
+const ideInfo = {
+ Ide: 'ydb',
+ IdeVersion: '1',
+ PluginFamily: 'ydb',
+ PluginVersion: '0.2',
+};
+
+export class CodeAssistAPI extends BaseYdbAPI {
+ getPath(path: string) {
+ return `${CODE_ASSISTANT_BACKEND ?? ''}${path}`;
+ }
+
+ getCodeAssistSuggestions(data: PromptFile[]): Promise {
+ return this.post(
+ this.getPath('/code-assist-suggestion'),
+ {
+ Files: data,
+ ContextCreateType: 1,
+ IdeInfo: ideInfo,
+ },
+ null,
+ {
+ concurrentId: 'code-assist-suggestion',
+ collectRequest: false,
+ },
+ );
+ }
+
+ sendCodeAssistTelemetry(data: TelemetryEvent): Promise {
+ return this.post(this.getPath('/code-assist-telemetry'), data, null, {
+ concurrentId: 'code-assist-telemetry',
+ collectRequest: false,
+ });
+ }
+
+ sendCodeAssistOpenTabs(data: TelemetryOpenTabs): Promise {
+ return this.post(
+ this.getPath('/code-assist-telemetry'),
+ {OpenTabs: {Tabs: data, IdeInfo: ideInfo}},
+ null,
+ {
+ concurrentId: 'code-assist-telemetry',
+ collectRequest: false,
+ },
+ );
+ }
+}
diff --git a/src/services/api/index.ts b/src/services/api/index.ts
index fbd795c2d3..2e76b26cba 100644
--- a/src/services/api/index.ts
+++ b/src/services/api/index.ts
@@ -1,6 +1,7 @@
import type {AxiosRequestConfig} from 'axios';
import {AuthAPI} from './auth';
+import {CodeAssistAPI} from './codeAssistant';
import {MetaAPI} from './meta';
import {OperationAPI} from './operation';
import {PDiskAPI} from './pdisk';
@@ -22,11 +23,13 @@ export class YdbEmbeddedAPI {
vdisk: VDiskAPI;
viewer: ViewerAPI;
meta?: MetaAPI;
+ codeAssistant?: CodeAssistAPI;
constructor({config, webVersion}: {config: AxiosRequestConfig; webVersion?: boolean}) {
this.auth = new AuthAPI({config});
if (webVersion) {
this.meta = new MetaAPI({config});
+ this.codeAssistant = new CodeAssistAPI({config});
}
this.operation = new OperationAPI({config});
this.pdisk = new PDiskAPI({config});
diff --git a/src/services/codeCompletion/CodeCompletionService.ts b/src/services/codeCompletion/CodeCompletionService.ts
new file mode 100644
index 0000000000..e6a98f9345
--- /dev/null
+++ b/src/services/codeCompletion/CodeCompletionService.ts
@@ -0,0 +1,280 @@
+import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+
+import {getPromptFileContent} from './promptContent';
+import type {
+ CodeCompletionConfig,
+ DiscardReason,
+ EnrichedCompletion,
+ ICodeCompletionAPI,
+ ICodeCompletionService,
+ ITelemetryService,
+ InternalSuggestion,
+} from './types';
+
+const DEFAULT_CONFIG: Required = {
+ debounceTime: 200,
+ textLimits: {
+ beforeCursor: 8000,
+ afterCursor: 1000,
+ },
+ telemetry: {
+ enabled: true,
+ },
+ suggestionCache: {
+ enabled: true,
+ },
+};
+
+export class CodeCompletionService implements ICodeCompletionService {
+ private prevSuggestions: InternalSuggestion[] = [];
+ private timer: number | null = null;
+ private readonly api: ICodeCompletionAPI;
+ private readonly telemetry: ITelemetryService;
+ private readonly config: Required;
+
+ constructor(
+ api: ICodeCompletionAPI,
+ telemetry: ITelemetryService,
+ userConfig?: CodeCompletionConfig,
+ ) {
+ this.api = api;
+ this.telemetry = telemetry;
+ // Merge user config with defaults, ensuring all properties exist
+ this.config = {
+ ...DEFAULT_CONFIG,
+ ...userConfig,
+ textLimits: {
+ ...DEFAULT_CONFIG.textLimits,
+ ...(userConfig?.textLimits || {}),
+ },
+ telemetry: {
+ ...DEFAULT_CONFIG.telemetry,
+ ...(userConfig?.telemetry || {}),
+ },
+ suggestionCache: {
+ ...DEFAULT_CONFIG.suggestionCache,
+ ...(userConfig?.suggestionCache || {}),
+ },
+ };
+ }
+
+ handleItemDidShow(
+ _completions: monaco.languages.InlineCompletions,
+ item: EnrichedCompletion,
+ ) {
+ if (!this.config.suggestionCache.enabled) {
+ return;
+ }
+
+ for (const suggests of this.prevSuggestions) {
+ for (const completion of suggests.items) {
+ if (completion.pristine === item.pristine) {
+ suggests.shownCount++;
+ break;
+ }
+ }
+ }
+ }
+
+ async provideInlineCompletions(
+ model: monaco.editor.ITextModel,
+ position: monaco.Position,
+ _context: monaco.languages.InlineCompletionContext,
+ _token: monaco.CancellationToken,
+ ) {
+ if (this.config.suggestionCache.enabled) {
+ const cachedCompletions = this.getCachedCompletion(model, position);
+ if (cachedCompletions.length) {
+ return {items: cachedCompletions};
+ }
+ }
+
+ while (this.prevSuggestions.length > 0) {
+ this.dismissCompletion(this.prevSuggestions.pop());
+ }
+ const {suggestions, requestId} = await this.getSuggestions(model, position);
+
+ this.prevSuggestions = [{items: suggestions, shownCount: 0, requestId}];
+ return {
+ items: suggestions,
+ };
+ }
+
+ handlePartialAccept(
+ _completions: monaco.languages.InlineCompletions,
+ item: monaco.languages.InlineCompletion,
+ acceptedLetters: number,
+ ) {
+ const {command} = item;
+ const commandArguments = command?.arguments?.[0] ?? {};
+ const {suggestionText, requestId, prevWordLength = 0} = commandArguments;
+ const cachedSuggestions = this.prevSuggestions.find((el) => {
+ return el.items.some((item) => item.pristine === suggestionText);
+ });
+ if (requestId && suggestionText && typeof item.insertText === 'string') {
+ const acceptedText = item.insertText.slice(prevWordLength, acceptedLetters);
+ if (acceptedText) {
+ if (cachedSuggestions) {
+ cachedSuggestions.wasAccepted = true;
+ }
+ this.telemetry.sendAcceptTelemetry(requestId, acceptedText);
+ }
+ }
+ }
+
+ handleAccept({requestId, suggestionText}: {requestId: string; suggestionText: string}) {
+ this.emptyCache();
+ this.telemetry.sendAcceptTelemetry(requestId, suggestionText);
+ }
+
+ commandDiscard(
+ reason: DiscardReason = 'OnCancel',
+ editor: monaco.editor.IStandaloneCodeEditor,
+ ): void {
+ while (this.prevSuggestions.length > 0) {
+ this.discardCompletion(reason, this.prevSuggestions.pop());
+ }
+ editor.trigger(undefined, 'editor.action.inlineSuggest.hide', undefined);
+ }
+
+ emptyCache() {
+ this.prevSuggestions = [];
+ }
+
+ freeInlineCompletions(): void {
+ // This method is required by Monaco's InlineCompletionsProvider interface
+ // but we don't need to do anything here since we handle cleanup in other methods
+ }
+
+ private getCachedCompletion(
+ model: monaco.editor.ITextModel,
+ position: monaco.Position,
+ ): EnrichedCompletion[] {
+ const completions: EnrichedCompletion[] = [];
+ for (const suggests of this.prevSuggestions) {
+ for (const completion of suggests.items) {
+ if (!completion.range) {
+ continue;
+ }
+ if (
+ position.lineNumber < completion.range.startLineNumber ||
+ position.column < completion.range.startColumn
+ ) {
+ continue;
+ }
+ const startCompletionPosition = new monaco.Position(
+ completion.range.startLineNumber,
+ completion.range.startColumn,
+ );
+ const startOffset = model.getOffsetAt(startCompletionPosition);
+ const endOffset = startOffset + completion.insertText.toString().length;
+ const positionOffset = model.getOffsetAt(position);
+ if (positionOffset > endOffset) {
+ continue;
+ }
+
+ const completionReplaceText = completion.insertText
+ .toString()
+ .slice(0, positionOffset - startOffset);
+
+ const newRange = new monaco.Range(
+ completion.range.startLineNumber,
+ completion.range.startColumn,
+ position.lineNumber,
+ position.column,
+ );
+ const currentReplaceText = model.getValueInRange(newRange);
+ if (completionReplaceText.toLowerCase() === currentReplaceText.toLowerCase()) {
+ completions.push({
+ insertText:
+ currentReplaceText +
+ completion.insertText.toString().slice(positionOffset - startOffset),
+ range: newRange,
+ command: completion.command,
+ pristine: completion.pristine,
+ });
+ }
+ }
+ }
+ return completions;
+ }
+
+ private async getSuggestions(model: monaco.editor.ITextModel, position: monaco.Position) {
+ if (this.timer) {
+ window.clearTimeout(this.timer);
+ }
+ await new Promise((r) => {
+ this.timer = window.setTimeout(r, this.config.debounceTime);
+ });
+ let suggestions: EnrichedCompletion[] = [];
+ let requestId = '';
+ try {
+ const data = getPromptFileContent(model, position, {
+ beforeCursor: this.config.textLimits.beforeCursor,
+ afterCursor: this.config.textLimits.afterCursor,
+ });
+ if (!data) {
+ return {suggestions: []};
+ }
+
+ const codeAssistSuggestions = await this.api.getCodeAssistSuggestions(data);
+ requestId = codeAssistSuggestions.RequestId;
+ const {word, startColumn: lastWordStartColumn} = model.getWordUntilPosition(position);
+ suggestions = codeAssistSuggestions.Suggests.map((el) => {
+ const suggestionText = el.Text;
+ const label = word + suggestionText;
+ return {
+ label: label,
+ sortText: 'a',
+ insertText: label,
+ pristine: suggestionText,
+ range: new monaco.Range(
+ position.lineNumber,
+ lastWordStartColumn,
+ position.lineNumber,
+ position.column,
+ ),
+ command: {
+ id: 'acceptCodeAssistCompletion',
+ title: '',
+ arguments: [
+ {
+ requestId,
+ suggestionText: suggestionText,
+ prevWordLength: word.length,
+ },
+ ],
+ },
+ };
+ });
+ } catch (err) {}
+ return {suggestions, requestId};
+ }
+
+ private discardCompletion(reason: DiscardReason, completion?: InternalSuggestion): void {
+ if (completion === undefined) {
+ return;
+ }
+ const {requestId, items, shownCount} = completion;
+ if (!requestId || !items.length) {
+ return;
+ }
+ for (const item of items) {
+ this.telemetry.sendDeclineTelemetry(requestId, item.pristine, reason, shownCount);
+ }
+ }
+
+ private dismissCompletion(completion?: InternalSuggestion): void {
+ if (completion === undefined) {
+ return;
+ }
+ const {requestId, items, shownCount, wasAccepted} = completion;
+
+ if (!requestId || !items.length || !shownCount || wasAccepted) {
+ return;
+ }
+ for (const item of items) {
+ this.telemetry.sendIgnoreTelemetry(requestId, item.pristine);
+ }
+ }
+}
diff --git a/src/services/codeCompletion/README.md b/src/services/codeCompletion/README.md
new file mode 100644
index 0000000000..38528cef00
--- /dev/null
+++ b/src/services/codeCompletion/README.md
@@ -0,0 +1,301 @@
+# Code Completion Service
+
+This module provides inline code completion functionality for the YDB Query Editor using Monaco Editor. It implements a sophisticated completion system with telemetry tracking and command integration.
+
+## Architecture Overview
+
+The code completion system consists of several key components working together:
+
+### Core Components
+
+1. **CodeCompletionService** (`CodeCompletionService.ts`)
+
+ - Implements Monaco's `InlineCompletionsProvider` interface
+ - Manages completion suggestions and their lifecycle
+ - Handles caching of suggestions
+ - Processes user interactions with suggestions (accept/decline/ignore)
+
+2. **TelemetryService** (`TelemetryService.ts`)
+
+ - Tracks completion usage metrics
+ - Records acceptance, decline, and ignore events
+ - Captures timing and interaction data
+ - Configurable telemetry collection
+
+3. **PromptContent** (`promptContent.ts`)
+
+ - Handles extraction of code context for suggestions
+ - Manages text fragments before and after cursor
+ - Implements text length limits and cursor positioning
+ - Creates structured prompt data for API requests
+
+4. **Factory** (`factory.ts`)
+
+ - Creates and configures the completion provider
+ - Wires together the completion and telemetry services
+ - Manages configuration defaults and merging
+
+5. **Command Registration** (`registerCommands.ts`)
+ - Registers Monaco editor commands for completion actions
+ - Handles accept/decline completion commands
+
+### Integration Points
+
+1. **Monaco Editor Integration**
+
+ - Registers the completion provider with Monaco
+ - Manages provider lifecycle
+ - Integrates with YQL language support
+
+2. **Query Editor Integration**
+ - Initializes code completion when editor mounts
+ - Registers completion commands
+ - Handles user interactions
+
+## Data Flow
+
+1. **Prompt Generation**
+
+ ```
+ Editor Change -> PromptContent.getPromptFileContent
+ -> Text Fragments with Cursor Position -> API Request
+ ```
+
+2. **Suggestion Generation**
+
+ ```
+ Prompt Data -> CodeCompletionService.provideInlineCompletions
+ -> API Request -> Suggestions Returned -> Monaco Display
+ ```
+
+3. **Suggestion Acceptance**
+
+ ```
+ User Accept -> handleAccept -> TelemetryService.sendAcceptTelemetry
+ -> API Telemetry Event
+ ```
+
+4. **Suggestion Rejection**
+ ```
+ User Decline -> commandDiscard -> TelemetryService.sendDeclineTelemetry
+ -> API Telemetry Event
+ ```
+
+## Configuration Options
+
+The service accepts a configuration object with these options:
+
+```typescript
+interface CodeCompletionConfig {
+ // Performance settings
+ debounceTime?: number; // Time in ms to debounce API calls (default: 200)
+
+ // Text limits
+ textLimits?: {
+ beforeCursor?: number; // Characters to include before cursor (default: 8000)
+ afterCursor?: number; // Characters to include after cursor (default: 1000)
+ };
+
+ // Telemetry settings
+ telemetry?: {
+ enabled?: boolean; // Whether to enable telemetry (default: true)
+ };
+
+ // Cache settings
+ suggestionCache?: {
+ enabled?: boolean; // Whether to enable suggestion caching (default: true)
+ };
+}
+```
+
+## Key Features
+
+1. **Prompt Generation**
+
+ - Extracts relevant code context around cursor
+ - Implements smart text truncation (8000 chars before, 1000 after cursor)
+ - Maintains cursor position information
+ - Creates structured prompt format for API
+
+2. **Suggestion Caching**
+
+ - Caches suggestions to reduce API calls
+ - Tracks suggestion display count
+ - Manages suggestion lifecycle
+ - Configurable caching behavior
+
+3. **Telemetry**
+
+ - Tracks suggestion acceptance rate
+ - Records user interaction patterns
+ - Monitors suggestion quality
+ - Configurable telemetry collection
+
+4. **Command Integration**
+ - Keyboard shortcuts for completion actions
+ - Context menu integration
+ - Custom commands for completion workflow
+
+## API Interface
+
+The completion service requires an API implementation with these methods:
+
+```typescript
+interface ICodeCompletionAPI {
+ getCodeAssistSuggestions(data: PromptFile[]): Promise;
+ sendCodeAssistTelemetry(data: TelemetryEvent): Promise;
+}
+
+interface Suggestions {
+ Suggests: Suggestion[];
+ RequestId: string;
+}
+
+interface Suggestion {
+ Text: string;
+}
+```
+
+## Prompt Format
+
+The prompt generation creates structured data in this format:
+
+```typescript
+interface PromptFile {
+ Path: string;
+ Fragments: PromptFragment[];
+ Cursor: PromptPosition;
+}
+
+interface PromptFragment {
+ Text: string;
+ Start: PromptPosition;
+ End: PromptPosition;
+}
+
+interface PromptPosition {
+ Ln: number;
+ Col: number;
+}
+```
+
+## Telemetry Events
+
+The system tracks three types of events:
+
+1. **Acceptance Events**
+
+ ```typescript
+ interface AcceptSuggestionEvent {
+ Accepted: {
+ RequestId: string;
+ Timestamp: number;
+ AcceptedText: string;
+ ConvertedText: string;
+ };
+ }
+ ```
+
+2. **Discard Events**
+
+ ```typescript
+ interface DiscardSuggestionEvent {
+ Discarded: {
+ RequestId: string;
+ Timestamp: number;
+ DiscardReason: 'OnCancel';
+ DiscardedText: string;
+ CacheHitCount: number;
+ };
+ }
+ ```
+
+3. **Ignore Events**
+ ```typescript
+ interface IgnoreSuggestionEvent {
+ Ignored: {
+ RequestId: string;
+ Timestamp: number;
+ IgnoredText: string;
+ };
+ }
+ ```
+
+## Usage Example
+
+```typescript
+import {createCompletionProvider, registerCompletionCommands} from './codeCompletion';
+
+// Create API implementation
+const api: ICodeCompletionAPI = {
+ getCodeAssistSuggestions: async (data) => { ... },
+ sendCodeAssistTelemetry: async (data) => { ... }
+};
+
+// Configure the service
+const config: CodeCompletionConfig = {
+ debounceTime: 200,
+ textLimits: {
+ beforeCursor: 8000,
+ afterCursor: 1000
+ },
+ telemetry: {
+ enabled: true
+ },
+ suggestionCache: {
+ enabled: true
+ }
+};
+
+// Create provider
+const completionProvider = createCompletionProvider(api, config);
+
+// Register with Monaco
+monaco.languages.registerInlineCompletionsProvider('yql', completionProvider);
+
+// Register commands
+registerCompletionCommands(monaco, completionProvider, editor);
+```
+
+## Best Practices
+
+1. **Prompt Generation**
+
+ - Consider text limits for optimal performance
+ - Maintain cursor position accuracy
+ - Handle edge cases in text extraction
+
+2. **Error Handling**
+
+ - All API calls should be wrapped in try-catch blocks
+ - Failed suggestions should not break the editor
+ - Telemetry failures should be logged but not impact user experience
+
+3. **Performance**
+
+ - Suggestions are throttled to prevent excessive API calls
+ - Caching reduces server load
+ - Completion provider is disposed when not needed
+
+4. **User Experience**
+ - Suggestions appear inline with minimal delay
+ - Clear feedback for acceptance/rejection
+ - Non-intrusive telemetry collection
+
+## Contributing
+
+When modifying the code completion system:
+
+1. Ensure all telemetry events are properly tracked
+2. Maintain backward compatibility with existing API
+3. Update tests for new functionality
+4. Consider performance implications of changes
+5. Document new features or changes
+6. Follow text limit guidelines in prompt generation
+
+## Related Components
+
+- Monaco Editor Configuration
+- YQL Language Support
+- Query Editor Implementation
+- Code Assistant API Integration
diff --git a/src/services/codeCompletion/TelemetryService.ts b/src/services/codeCompletion/TelemetryService.ts
new file mode 100644
index 0000000000..53b0b6e334
--- /dev/null
+++ b/src/services/codeCompletion/TelemetryService.ts
@@ -0,0 +1,69 @@
+import type {CodeCompletionConfig, DiscardReason, ITelemetryService, TelemetryEvent} from './types';
+
+export class TelemetryService implements ITelemetryService {
+ private readonly sendTelemetry: (data: TelemetryEvent) => Promise;
+ private readonly config: Required>;
+
+ constructor(
+ sendTelemetry: (data: TelemetryEvent) => Promise,
+ config: CodeCompletionConfig = {},
+ ) {
+ this.sendTelemetry = sendTelemetry;
+ this.config = {
+ enabled: config.telemetry?.enabled ?? true,
+ };
+ }
+
+ sendAcceptTelemetry(requestId: string, acceptedText: string): void {
+ const data: TelemetryEvent = {
+ Accepted: {
+ RequestId: requestId,
+ Timestamp: Date.now(),
+ AcceptedText: acceptedText,
+ ConvertedText: acceptedText,
+ },
+ };
+ this.send(data);
+ }
+
+ sendDeclineTelemetry(
+ requestId: string,
+ suggestionText: string,
+ reason: DiscardReason,
+ hitCount: number,
+ ): void {
+ const data: TelemetryEvent = {
+ Discarded: {
+ RequestId: requestId,
+ Timestamp: Date.now(),
+ DiscardReason: reason,
+ DiscardedText: suggestionText,
+ CacheHitCount: hitCount,
+ },
+ };
+ this.send(data);
+ }
+
+ sendIgnoreTelemetry(requestId: string, suggestionText: string): void {
+ const data: TelemetryEvent = {
+ Ignored: {
+ RequestId: requestId,
+ Timestamp: Date.now(),
+ IgnoredText: suggestionText,
+ },
+ };
+ this.send(data);
+ }
+
+ private send(data: TelemetryEvent): void {
+ if (!this.config.enabled) {
+ return;
+ }
+
+ try {
+ this.sendTelemetry(data);
+ } catch (e) {
+ console.error('Failed to send telemetry:', e);
+ }
+ }
+}
diff --git a/src/services/codeCompletion/factory.ts b/src/services/codeCompletion/factory.ts
new file mode 100644
index 0000000000..ecd41e5637
--- /dev/null
+++ b/src/services/codeCompletion/factory.ts
@@ -0,0 +1,54 @@
+import {CodeCompletionService} from './CodeCompletionService';
+import {TelemetryService} from './TelemetryService';
+import type {CodeCompletionConfig, ICodeCompletionAPI} from './types';
+
+const DEFAULT_CONFIG: Required = {
+ debounceTime: 200,
+ textLimits: {
+ beforeCursor: 8000,
+ afterCursor: 1000,
+ },
+ telemetry: {
+ enabled: true,
+ },
+ suggestionCache: {
+ enabled: true,
+ },
+};
+
+function mergeWithDefaults(userConfig?: CodeCompletionConfig): Required {
+ if (!userConfig) {
+ return DEFAULT_CONFIG;
+ }
+
+ return {
+ debounceTime: userConfig.debounceTime ?? DEFAULT_CONFIG.debounceTime,
+ textLimits: {
+ beforeCursor:
+ userConfig.textLimits?.beforeCursor ?? DEFAULT_CONFIG.textLimits.beforeCursor,
+ afterCursor:
+ userConfig.textLimits?.afterCursor ?? DEFAULT_CONFIG.textLimits.afterCursor,
+ },
+ telemetry: {
+ enabled: userConfig.telemetry?.enabled ?? DEFAULT_CONFIG.telemetry.enabled,
+ },
+ suggestionCache: {
+ enabled: userConfig.suggestionCache?.enabled ?? DEFAULT_CONFIG.suggestionCache.enabled,
+ },
+ };
+}
+
+export function createCompletionProvider(api: ICodeCompletionAPI, config?: CodeCompletionConfig) {
+ const mergedConfig = mergeWithDefaults(config);
+
+ const telemetryService = new TelemetryService(
+ mergedConfig.telemetry.enabled
+ ? (data) => api.sendCodeAssistTelemetry(data)
+ : () => Promise.resolve(undefined),
+ mergedConfig,
+ );
+
+ return new CodeCompletionService(api, telemetryService, mergedConfig);
+}
+
+export {DEFAULT_CONFIG};
diff --git a/src/services/codeCompletion/index.ts b/src/services/codeCompletion/index.ts
new file mode 100644
index 0000000000..9de68053b6
--- /dev/null
+++ b/src/services/codeCompletion/index.ts
@@ -0,0 +1,14 @@
+// Export types needed for API and store integration
+export type {
+ ICodeCompletionAPI,
+ ICodeCompletionService,
+ CodeCompletionConfig,
+ PromptFile,
+ Suggestions,
+ TelemetryEvent,
+ TelemetryOpenTabs,
+} from './types';
+
+// Export functions needed for Monaco editor integration
+export {createCompletionProvider} from './factory';
+export {registerCompletionCommands} from './registerCommands';
diff --git a/src/services/codeCompletion/promptContent.ts b/src/services/codeCompletion/promptContent.ts
new file mode 100644
index 0000000000..e2fc785907
--- /dev/null
+++ b/src/services/codeCompletion/promptContent.ts
@@ -0,0 +1,75 @@
+import type Monaco from 'monaco-editor';
+import {v4} from 'uuid';
+
+import type {PromptFile} from './types';
+
+export interface TextLimits {
+ beforeCursor: number;
+ afterCursor: number;
+}
+
+const DEFAULT_LIMITS: Required = {
+ beforeCursor: 8_000,
+ afterCursor: 1_000,
+};
+
+const sessionId = v4();
+
+export function getPromptFileContent(
+ model: Monaco.editor.ITextModel,
+ position: Monaco.Position,
+ limits?: Partial,
+): PromptFile[] | undefined {
+ // Merge with defaults to ensure we always have valid numbers
+ const finalLimits: Required = {
+ ...DEFAULT_LIMITS,
+ ...limits,
+ };
+
+ const linesContent = model.getLinesContent();
+ const prevTextInCurrentLine = linesContent[position.lineNumber - 1].slice(
+ 0,
+ position.column - 1,
+ );
+ const postTextInCurrentLine = linesContent[position.lineNumber - 1].slice(position.column - 1);
+ const prevText = linesContent
+ .slice(0, position.lineNumber - 1)
+ .concat([prevTextInCurrentLine])
+ .join('\n');
+ const postText = [postTextInCurrentLine]
+ .concat(linesContent.slice(position.lineNumber))
+ .join('\n');
+ const cursorPostion = {Ln: position.lineNumber, Col: position.column};
+
+ const fragments = [];
+ if (prevText) {
+ fragments.push({
+ Text:
+ prevText.length > finalLimits.beforeCursor
+ ? prevText.slice(prevText.length - finalLimits.beforeCursor)
+ : prevText,
+ Start: {Ln: 1, Col: 1},
+ End: cursorPostion,
+ });
+ }
+ if (postText) {
+ fragments.push({
+ Text: postText.slice(0, finalLimits.afterCursor),
+ Start: cursorPostion,
+ End: {
+ Ln: linesContent.length,
+ Col: linesContent[linesContent.length - 1].length,
+ },
+ });
+ }
+
+ return fragments.length
+ ? [
+ {
+ Fragments: fragments,
+ Cursor: cursorPostion,
+ Path: `${sessionId}/query.yql`,
+ },
+ ]
+ : undefined;
+}
diff --git a/src/services/codeCompletion/registerCommands.ts b/src/services/codeCompletion/registerCommands.ts
new file mode 100644
index 0000000000..f3299dd685
--- /dev/null
+++ b/src/services/codeCompletion/registerCommands.ts
@@ -0,0 +1,24 @@
+import type * as monaco from 'monaco-editor';
+
+import type {ICodeCompletionService} from './types';
+
+export function registerCompletionCommands(
+ monacoInstance: typeof monaco,
+ completionService: ICodeCompletionService,
+ editor: monaco.editor.IStandaloneCodeEditor,
+) {
+ monacoInstance.editor.registerCommand('acceptCodeAssistCompletion', (_accessor, ...args) => {
+ const data = args[0] ?? {};
+ if (!data || typeof data !== 'object') {
+ return;
+ }
+ const {requestId, suggestionText} = data;
+ if (requestId && suggestionText) {
+ completionService.handleAccept({requestId, suggestionText});
+ }
+ });
+
+ monacoInstance.editor.registerCommand('declineCodeAssistCompletion', () => {
+ completionService.commandDiscard('OnCancel', editor);
+ });
+}
diff --git a/src/services/codeCompletion/types.ts b/src/services/codeCompletion/types.ts
new file mode 100644
index 0000000000..251c53f62d
--- /dev/null
+++ b/src/services/codeCompletion/types.ts
@@ -0,0 +1,142 @@
+import type * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+
+type IdeInfo = {
+ Ide: string;
+ IdeVersion: string;
+ PluginFamily: string;
+ PluginVersion: string;
+};
+
+export interface Prompt {
+ Files: PromptFile[];
+ ContextCreateType: ContextCreateType;
+ ForceSuggest?: boolean;
+ IdeInfo: IdeInfo;
+}
+
+export interface PromptPosition {
+ Ln: number;
+ Col: number;
+}
+
+export interface PromptFragment {
+ Text: string;
+ Start: PromptPosition;
+ End: PromptPosition;
+}
+
+export interface PromptFile {
+ Path: string;
+ Fragments: PromptFragment[];
+ Cursor: PromptPosition;
+}
+
+export type ContextCreateType = 1;
+
+export interface Suggestions {
+ Suggests: Suggestion[];
+ RequestId: string;
+}
+
+export type DiscardReason = 'OnCancel';
+
+export interface Suggestion {
+ Text: string;
+}
+
+export interface AcceptSuggestionEvent {
+ Accepted: {
+ RequestId: string;
+ Timestamp: number;
+ AcceptedText: string;
+ ConvertedText: string;
+ };
+}
+export interface DiscardSuggestionEvent {
+ Discarded: {
+ RequestId: string;
+ Timestamp: number;
+ DiscardReason: 'OnCancel';
+ DiscardedText: string;
+ CacheHitCount: number;
+ };
+}
+export interface IgnoreSuggestionEvent {
+ Ignored: {
+ RequestId: string;
+ Timestamp: number;
+ IgnoredText: string;
+ };
+}
+
+export type TelemetryEvent = AcceptSuggestionEvent | DiscardSuggestionEvent | IgnoreSuggestionEvent;
+
+type OpenTab = {
+ FileName: string;
+ Text: string;
+};
+
+export type TelemetryOpenTabs = OpenTab[];
+
+export interface ICodeCompletionAPI {
+ getCodeAssistSuggestions(data: PromptFile[]): Promise;
+ sendCodeAssistTelemetry(data: TelemetryEvent): Promise;
+}
+
+export interface ITelemetryService {
+ sendAcceptTelemetry(requestId: string, acceptedText: string): void;
+ sendDeclineTelemetry(
+ requestId: string,
+ suggestionText: string,
+ reason: DiscardReason,
+ hitCount: number,
+ ): void;
+ sendIgnoreTelemetry(requestId: string, suggestionText: string): void;
+}
+
+export interface EnrichedCompletion extends monaco.languages.InlineCompletion {
+ pristine: string;
+}
+
+export interface InternalSuggestion {
+ items: EnrichedCompletion[];
+ requestId?: string;
+ shownCount: number;
+ wasAccepted?: boolean;
+}
+
+export interface ICodeCompletionService extends monaco.languages.InlineCompletionsProvider {
+ handleItemDidShow(
+ completions: monaco.languages.InlineCompletions,
+ item: EnrichedCompletion,
+ ): void;
+ handlePartialAccept(
+ completions: monaco.languages.InlineCompletions,
+ item: monaco.languages.InlineCompletion,
+ acceptedLetters: number,
+ ): void;
+ handleAccept(params: {requestId: string; suggestionText: string}): void;
+ commandDiscard(reason: DiscardReason, editor: monaco.editor.IStandaloneCodeEditor): void;
+ emptyCache(): void;
+}
+
+export interface CodeCompletionConfig {
+ // Performance settings
+ debounceTime?: number; // Time in ms to debounce API calls (default: 200)
+
+ // Text limits
+ textLimits?: {
+ beforeCursor?: number; // Characters to include before cursor (default: 8000)
+ afterCursor?: number; // Characters to include after cursor (default: 1000)
+ };
+
+ // Telemetry settings
+ telemetry?: {
+ enabled?: boolean; // Whether to enable telemetry (default: true)
+ };
+
+ // Cache settings
+ suggestionCache?: {
+ enabled?: boolean; // Whether to enable suggestion caching (default: true)
+ };
+}
diff --git a/src/store/configureStore.ts b/src/store/configureStore.ts
index afc32d4637..07e80758ec 100644
--- a/src/store/configureStore.ts
+++ b/src/store/configureStore.ts
@@ -46,6 +46,7 @@ function _configureStore<
export const webVersion = window.web_version;
export const customBackend = window.custom_backend;
export const metaBackend = window.meta_backend;
+export const codeAssistantBackend = window.code_assistant_backend;
const isSingleClusterMode = `${metaBackend}` === 'undefined';
diff --git a/src/store/index.ts b/src/store/index.ts
index 38f228f33c..7b6d3a1768 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -6,6 +6,7 @@ export {
customBackend,
metaBackend,
webVersion,
+ codeAssistantBackend,
} from './configureStore';
export {rootReducer} from './reducers';
diff --git a/src/store/reducers/codeAssist.ts b/src/store/reducers/codeAssist.ts
new file mode 100644
index 0000000000..7d33e82942
--- /dev/null
+++ b/src/store/reducers/codeAssist.ts
@@ -0,0 +1,21 @@
+import type {TelemetryOpenTabs} from '../../services/codeCompletion';
+
+import {api} from './api';
+
+export const codeAssistApi = api.injectEndpoints({
+ endpoints: (build) => ({
+ sendOpenTabs: build.mutation({
+ queryFn: async (params: TelemetryOpenTabs) => {
+ try {
+ const data = await window.api.codeAssistant?.sendCodeAssistOpenTabs(params);
+ return {data};
+ } catch (error) {
+ return {error};
+ }
+ },
+ }),
+ }),
+ overrideExisting: 'throw',
+});
+
+export const {useSendOpenTabsMutation} = codeAssistApi;
diff --git a/src/types/window.d.ts b/src/types/window.d.ts
index a31c06ad28..236918267b 100644
--- a/src/types/window.d.ts
+++ b/src/types/window.d.ts
@@ -37,6 +37,7 @@ interface Window {
web_version?: boolean;
custom_backend?: string;
meta_backend?: string;
+ code_assistant_backend?: string;
userSettings?: import('../services/settings').SettingsObject;
systemSettings?: import('../services/settings').SettingsObject;
diff --git a/src/utils/monaco/yql/ydb.inlineCompletionProvider.ts b/src/utils/monaco/yql/ydb.inlineCompletionProvider.ts
new file mode 100644
index 0000000000..82b92b58fb
--- /dev/null
+++ b/src/utils/monaco/yql/ydb.inlineCompletionProvider.ts
@@ -0,0 +1,37 @@
+import * as monaco from 'monaco-editor';
+import {LANGUAGE_ID} from 'monaco-yql-languages/build/yql/yql.contribution';
+
+import {createCompletionProvider} from '../../../services/codeCompletion';
+import type {
+ CodeCompletionConfig,
+ ICodeCompletionAPI,
+ ICodeCompletionService,
+} from '../../../services/codeCompletion';
+
+let inlineProvider: monaco.IDisposable | undefined;
+
+function disableCodeSuggestions(): void {
+ if (inlineProvider) {
+ inlineProvider.dispose();
+ }
+}
+
+let completionProviderInstance: ICodeCompletionService | null = null;
+
+export function getCompletionProvider(): ICodeCompletionService | null {
+ return completionProviderInstance;
+}
+
+export function registerInlineCompletionProvider(
+ api: ICodeCompletionAPI,
+ config?: CodeCompletionConfig,
+) {
+ disableCodeSuggestions();
+
+ completionProviderInstance = createCompletionProvider(api, config);
+
+ inlineProvider = monaco.languages.registerInlineCompletionsProvider(
+ LANGUAGE_ID,
+ completionProviderInstance,
+ );
+}