deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/ai-editor/.eslintrc.js
Normal file
10
packages/ai-editor/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
31
packages/ai-editor/README.md
Normal file
31
packages/ai-editor/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - AI EDITOR EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/ai-editor` extension brings AI-powered code actions and editor context interaction to Theia. It allows users to interact with AI directly in the editor for code explanations, fixes, and suggestions using AI agents.
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [API documentation for `@theia/ai-editor`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_ai-editor.html)
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
57
packages/ai-editor/package.json
Normal file
57
packages/ai-editor/package.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "@theia/ai-editor",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia - AI Editor",
|
||||
"dependencies": {
|
||||
"@theia/core": "1.68.0",
|
||||
"@theia/editor": "1.68.0",
|
||||
"@theia/filesystem": "1.68.0",
|
||||
"@theia/monaco": "1.68.0",
|
||||
"@theia/monaco-editor-core": "1.96.302",
|
||||
"@theia/ai-core": "1.68.0",
|
||||
"@theia/ai-chat": "1.68.0",
|
||||
"@theia/ai-chat-ui": "1.68.0",
|
||||
"@theia/workspace": "1.68.0"
|
||||
},
|
||||
"main": "lib/common",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"frontend": "lib/browser/ai-editor-frontend-module"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"theia-extension"
|
||||
],
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"data",
|
||||
"lib",
|
||||
"src",
|
||||
"style"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0"
|
||||
},
|
||||
"nyc": {
|
||||
"extends": "../../configs/nyc.json"
|
||||
}
|
||||
}
|
||||
119
packages/ai-editor/src/browser/ai-code-action-provider.ts
Normal file
119
packages/ai-editor/src/browser/ai-code-action-provider.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 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 { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { CommandService } from '@theia/core/lib/common/command';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
|
||||
import { AIActivationService } from '@theia/ai-core/lib/browser/ai-activation-service';
|
||||
import { nls } from '@theia/core';
|
||||
|
||||
export const AI_EDITOR_SEND_TO_CHAT = {
|
||||
id: 'ai-editor.sendToChat',
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class AICodeActionProvider implements FrontendApplicationContribution {
|
||||
|
||||
@inject(CommandService)
|
||||
protected readonly commandService: CommandService;
|
||||
|
||||
@inject(MonacoEditorService)
|
||||
protected readonly monacoEditorService: MonacoEditorService;
|
||||
|
||||
@inject(AIActivationService)
|
||||
protected readonly activationService: AIActivationService;
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
onStart(): void {
|
||||
this.registerCodeActionProvider();
|
||||
|
||||
// Listen to AI activation changes and re-register the provider
|
||||
this.activationService.onDidChangeActiveStatus(() => {
|
||||
this.toDispose.dispose();
|
||||
this.registerCodeActionProvider();
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
protected registerCodeActionProvider(): void {
|
||||
if (!this.activationService.isActive) {
|
||||
// AI is disabled, don't register the provider
|
||||
return;
|
||||
}
|
||||
|
||||
const disposable = monaco.languages.registerCodeActionProvider('*', {
|
||||
provideCodeActions: (model, range, context, token) => {
|
||||
// Double-check activation status in the provider
|
||||
if (!this.activationService.isActive) {
|
||||
return { actions: [], dispose: () => { } };
|
||||
}
|
||||
|
||||
// Filter for error markers only
|
||||
const errorMarkers = context.markers.filter(marker =>
|
||||
marker.severity === monaco.MarkerSeverity.Error);
|
||||
|
||||
if (errorMarkers.length === 0) {
|
||||
return { actions: [], dispose: () => { } };
|
||||
}
|
||||
|
||||
const actions: monaco.languages.CodeAction[] = [];
|
||||
|
||||
// Create code actions for each error marker: Fix with AI and Explain with AI
|
||||
errorMarkers.forEach(marker => {
|
||||
actions.push({
|
||||
title: nls.localizeByDefault('Fix with AI'),
|
||||
diagnostics: [marker],
|
||||
isAI: true,
|
||||
kind: 'quickfix',
|
||||
command: {
|
||||
id: AI_EDITOR_SEND_TO_CHAT.id,
|
||||
title: nls.localizeByDefault('Fix with AI'),
|
||||
arguments: [{
|
||||
prompt: `@Coder ${nls.localize('theia/ai/editor/fixWithAI/prompt', 'Help to fix this error')}: "${marker.message}"`
|
||||
}]
|
||||
}
|
||||
});
|
||||
actions.push({
|
||||
title: nls.localize('theia/ai/editor/explainWithAI/title', 'Explain with AI'),
|
||||
diagnostics: [marker],
|
||||
kind: 'quickfix',
|
||||
isAI: true,
|
||||
command: {
|
||||
id: AI_EDITOR_SEND_TO_CHAT.id,
|
||||
title: nls.localize('theia/ai/editor/explainWithAI/title', 'Explain with AI'),
|
||||
arguments: [{
|
||||
prompt: `@Architect ${nls.localize('theia/ai/editor/explainWithAI/prompt', 'Explain this error')}: "${marker.message}"`
|
||||
}]
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
actions: actions,
|
||||
dispose: () => { }
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
this.toDispose.push(disposable);
|
||||
}
|
||||
}
|
||||
161
packages/ai-editor/src/browser/ai-editor-command-contribution.ts
Normal file
161
packages/ai-editor/src/browser/ai-editor-command-contribution.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 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 { ChatAgentLocation, ChatRequest, ChatService } from '@theia/ai-chat';
|
||||
import { AICommandHandlerFactory, ENABLE_AI_CONTEXT_KEY } from '@theia/ai-core/lib/browser';
|
||||
import { isObject, isString, MenuContribution, MenuModelRegistry } from '@theia/core';
|
||||
import { ApplicationShell, codicon, KeybindingContribution, KeybindingRegistry } from '@theia/core/lib/browser';
|
||||
import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { EditorContextMenu, EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { MonacoCommandRegistry, MonacoEditorCommandHandler } from '@theia/monaco/lib/browser/monaco-command-registry';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { AskAIInputMonacoZoneWidget } from './ask-ai-input-monaco-zone-widget';
|
||||
import { AskAIInputFactory } from './ask-ai-input-widget';
|
||||
|
||||
export namespace AI_EDITOR_COMMANDS {
|
||||
export const AI_EDITOR_ASK_AI: Command = Command.toLocalizedCommand({
|
||||
id: 'ai-editor.contextAction',
|
||||
label: 'Ask AI',
|
||||
iconClass: codicon('sparkle')
|
||||
}, 'theia/ai-editor/contextMenu');
|
||||
export const AI_EDITOR_SEND_TO_CHAT: Command = Command.toLocalizedCommand({
|
||||
id: 'ai-editor.sendToChat',
|
||||
label: 'Send to AI Chat'
|
||||
}, 'theia/ai-editor/sendToChat');
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class AiEditorCommandContribution implements CommandContribution, MenuContribution, KeybindingContribution {
|
||||
|
||||
@inject(MonacoCommandRegistry)
|
||||
protected readonly monacoCommandRegistry: MonacoCommandRegistry;
|
||||
|
||||
@inject(ChatService)
|
||||
protected readonly chatService: ChatService;
|
||||
|
||||
@inject(AICommandHandlerFactory)
|
||||
protected readonly commandHandlerFactory: AICommandHandlerFactory;
|
||||
|
||||
@inject(AskAIInputFactory)
|
||||
protected readonly askAIInputFactory: AskAIInputFactory;
|
||||
|
||||
@inject(ApplicationShell)
|
||||
protected readonly shell: ApplicationShell;
|
||||
|
||||
protected askAiInputWidget: AskAIInputMonacoZoneWidget | undefined;
|
||||
|
||||
registerCommands(registry: CommandRegistry): void {
|
||||
registry.registerCommand(AI_EDITOR_COMMANDS.AI_EDITOR_ASK_AI);
|
||||
registry.registerCommand(AI_EDITOR_COMMANDS.AI_EDITOR_SEND_TO_CHAT);
|
||||
|
||||
this.monacoCommandRegistry.registerHandler(AI_EDITOR_COMMANDS.AI_EDITOR_ASK_AI.id, this.wrapMonacoHandler(this.showInputWidgetHandler()));
|
||||
this.monacoCommandRegistry.registerHandler(AI_EDITOR_COMMANDS.AI_EDITOR_SEND_TO_CHAT.id, this.wrapMonacoHandler(this.sendToChatHandler()));
|
||||
}
|
||||
|
||||
protected showInputWidgetHandler(): MonacoEditorCommandHandler {
|
||||
return {
|
||||
execute: (editor: MonacoEditor) => {
|
||||
this.showInputWidget(editor);
|
||||
},
|
||||
isEnabled: (editor: MonacoEditor) =>
|
||||
this.shell.currentWidget instanceof EditorWidget && (this.shell.currentWidget as EditorWidget).editor === editor
|
||||
};
|
||||
}
|
||||
|
||||
private showInputWidget(editor: MonacoEditor): void {
|
||||
this.cleanupInputWidget();
|
||||
|
||||
// Create the input widget using the factory
|
||||
this.askAiInputWidget = new AskAIInputMonacoZoneWidget(editor.getControl(), this.askAIInputFactory);
|
||||
|
||||
this.askAiInputWidget.onSubmit(request => {
|
||||
this.createNewChatSession(request);
|
||||
this.cleanupInputWidget();
|
||||
});
|
||||
|
||||
const line = editor.getControl().getPosition()?.lineNumber ?? 1;
|
||||
|
||||
this.askAiInputWidget.showAtLine(line);
|
||||
|
||||
this.askAiInputWidget.onCancel(() => {
|
||||
this.cleanupInputWidget();
|
||||
});
|
||||
}
|
||||
|
||||
private cleanupInputWidget(): void {
|
||||
if (this.askAiInputWidget) {
|
||||
this.askAiInputWidget.dispose();
|
||||
this.askAiInputWidget = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected sendToChatHandler(): MonacoEditorCommandHandler {
|
||||
return {
|
||||
execute: (_editor: MonacoEditor, ...args: unknown[]) => {
|
||||
if (containsPrompt(args)) {
|
||||
const prompt = args
|
||||
.filter(isPromptArg)
|
||||
.map(arg => arg.prompt)
|
||||
.join();
|
||||
this.createNewChatSession({ text: prompt } as ChatRequest);
|
||||
}
|
||||
},
|
||||
isEnabled: (_editor: MonacoEditor, ...args: unknown[]) => containsPrompt(args)
|
||||
};
|
||||
}
|
||||
|
||||
private createNewChatSession(request: ChatRequest): void {
|
||||
const session = this.chatService.createSession(ChatAgentLocation.Panel, { focus: true });
|
||||
this.chatService.sendRequest(session.id, {
|
||||
...request,
|
||||
text: `${request.text} #editorContext`,
|
||||
});
|
||||
}
|
||||
|
||||
protected wrapMonacoHandler(handler: MonacoEditorCommandHandler): MonacoEditorCommandHandler {
|
||||
const wrappedHandler = this.commandHandlerFactory(handler);
|
||||
return {
|
||||
execute: wrappedHandler.execute,
|
||||
isEnabled: wrappedHandler.isEnabled
|
||||
};
|
||||
}
|
||||
|
||||
registerMenus(menus: MenuModelRegistry): void {
|
||||
menus.registerMenuAction(EditorContextMenu.NAVIGATION, {
|
||||
commandId: AI_EDITOR_COMMANDS.AI_EDITOR_ASK_AI.id,
|
||||
label: AI_EDITOR_COMMANDS.AI_EDITOR_ASK_AI.label,
|
||||
icon: AI_EDITOR_COMMANDS.AI_EDITOR_ASK_AI.iconClass,
|
||||
when: ENABLE_AI_CONTEXT_KEY
|
||||
});
|
||||
}
|
||||
|
||||
registerKeybindings(registry: KeybindingRegistry): void {
|
||||
registry.registerKeybinding({
|
||||
command: AI_EDITOR_COMMANDS.AI_EDITOR_ASK_AI.id,
|
||||
keybinding: 'ctrlcmd+i',
|
||||
when: `${ENABLE_AI_CONTEXT_KEY} && editorFocus && !editorReadonly`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function containsPrompt(args: unknown[]): boolean {
|
||||
return args.some(arg => isPromptArg(arg));
|
||||
}
|
||||
|
||||
function isPromptArg(arg: unknown): arg is { prompt: string } {
|
||||
return isObject(arg) && 'prompt' in arg && isString(arg.prompt);
|
||||
}
|
||||
191
packages/ai-editor/src/browser/ai-editor-context-variable.ts
Normal file
191
packages/ai-editor/src/browser/ai-editor-context-variable.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 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 { AIVariable, AIVariableContext, AIVariableContribution, AIVariableResolutionRequest, AIVariableResolver, ResolvedAIContextVariable } from '@theia/ai-core';
|
||||
import { FrontendVariableService } from '@theia/ai-core/lib/browser';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { codiconArray } from '@theia/core/lib/browser';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { nls } from '@theia/core';
|
||||
|
||||
export const EDITOR_CONTEXT_VARIABLE: AIVariable = {
|
||||
id: 'editorContext',
|
||||
description: nls.localize('theia/ai/editor/editorContextVariable/description', 'Resolves editor specific context information'),
|
||||
name: 'editorContext',
|
||||
label: nls.localize('theia/ai/editor/editorContextVariable/label', 'EditorContext'),
|
||||
iconClasses: codiconArray('file'),
|
||||
isContextVariable: true,
|
||||
args: []
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class EditorContextVariableContribution implements AIVariableContribution, AIVariableResolver {
|
||||
|
||||
@inject(MonacoEditorProvider)
|
||||
protected readonly monacoEditors: MonacoEditorProvider;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
registerVariables(service: FrontendVariableService): void {
|
||||
service.registerResolver(EDITOR_CONTEXT_VARIABLE, this);
|
||||
}
|
||||
|
||||
async canResolve(request: AIVariableResolutionRequest, _: AIVariableContext): Promise<number> {
|
||||
return request.variable.name === EDITOR_CONTEXT_VARIABLE.name ? 1 : 0;
|
||||
}
|
||||
|
||||
async resolve(request: AIVariableResolutionRequest, _: AIVariableContext): Promise<ResolvedAIContextVariable | undefined> {
|
||||
const editor = this.monacoEditors.current;
|
||||
if (!editor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const model = editor.getControl().getModel();
|
||||
const selection = editor.getControl().getSelection();
|
||||
|
||||
if (!model || !selection) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Extract file information
|
||||
const uri = editor.getResourceUri();
|
||||
const languageId = model.getLanguageId();
|
||||
|
||||
// Extract selection information
|
||||
const selectedText = model.getValueInRange(selection);
|
||||
const hasSelection = !selection.isEmpty();
|
||||
|
||||
// Text position information
|
||||
const position = editor.getControl().getPosition();
|
||||
const lineNumber = position ? position.lineNumber : 0;
|
||||
const column = position ? position.column : 0;
|
||||
|
||||
// Get workspace-relative path
|
||||
const workspaceRelativePath = uri ? await this.workspaceService.getWorkspaceRelativePath(uri) : '';
|
||||
|
||||
// Create base context information
|
||||
const baseContext = {
|
||||
file: {
|
||||
uri: workspaceRelativePath,
|
||||
languageId,
|
||||
fileName: uri ? uri.path.base : ''
|
||||
},
|
||||
selection: {
|
||||
text: selectedText,
|
||||
isEmpty: !hasSelection,
|
||||
startLineNumber: selection.startLineNumber,
|
||||
startColumn: selection.startColumn,
|
||||
endLineNumber: selection.endLineNumber,
|
||||
endColumn: selection.endColumn
|
||||
},
|
||||
position: {
|
||||
lineNumber,
|
||||
column,
|
||||
lineContent: position ? model.getLineContent(position.lineNumber) : ''
|
||||
}
|
||||
};
|
||||
|
||||
const diagnostics = await this.getDiagnosticContext(editor);
|
||||
|
||||
const fullContext = {
|
||||
...baseContext,
|
||||
diagnostics
|
||||
};
|
||||
|
||||
const contextValue = JSON.stringify(fullContext, undefined, 2);
|
||||
|
||||
return {
|
||||
variable: request.variable,
|
||||
value: contextValue, // Simplified visible value
|
||||
contextValue // Full detailed context for AI processing
|
||||
};
|
||||
}
|
||||
protected getDiagnosticContext(editor: MonacoEditor): Record<string, unknown> {
|
||||
const model = editor.getControl().getModel();
|
||||
if (!model) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const markers = monaco.editor.getModelMarkers({ resource: model.uri });
|
||||
|
||||
if (markers.length === 0) {
|
||||
return {
|
||||
errorCount: 0,
|
||||
warningCount: 0,
|
||||
infoCount: 0,
|
||||
hintCount: 0,
|
||||
totalIssues: 0
|
||||
};
|
||||
}
|
||||
|
||||
const issues: Array<{
|
||||
line: number;
|
||||
column: number;
|
||||
severity: string;
|
||||
message: string;
|
||||
source?: string;
|
||||
}> = [];
|
||||
|
||||
const markerCounter = {
|
||||
[monaco.MarkerSeverity.Error]: 0,
|
||||
[monaco.MarkerSeverity.Warning]: 0,
|
||||
[monaco.MarkerSeverity.Info]: 0,
|
||||
[monaco.MarkerSeverity.Hint]: 0
|
||||
};
|
||||
|
||||
markers.forEach(marker => {
|
||||
markerCounter[marker.severity]++;
|
||||
|
||||
issues.push({
|
||||
line: marker.startLineNumber,
|
||||
column: marker.startColumn,
|
||||
severity: this.severityToString(marker.severity),
|
||||
message: marker.message,
|
||||
source: marker.source
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
diagnosticCounts: {
|
||||
errorCount: markerCounter[monaco.MarkerSeverity.Error],
|
||||
warningCount: markerCounter[monaco.MarkerSeverity.Warning],
|
||||
infoCount: markerCounter[monaco.MarkerSeverity.Info],
|
||||
hintCount: markerCounter[monaco.MarkerSeverity.Hint]
|
||||
},
|
||||
totalIssues: markers.length,
|
||||
issues
|
||||
};
|
||||
}
|
||||
|
||||
protected severityToString(severity: monaco.MarkerSeverity): string {
|
||||
switch (severity) {
|
||||
case monaco.MarkerSeverity.Error:
|
||||
return 'error';
|
||||
case monaco.MarkerSeverity.Warning:
|
||||
return 'warning';
|
||||
case monaco.MarkerSeverity.Info:
|
||||
return 'info';
|
||||
case monaco.MarkerSeverity.Hint:
|
||||
return 'hint';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
56
packages/ai-editor/src/browser/ai-editor-frontend-module.ts
Normal file
56
packages/ai-editor/src/browser/ai-editor-frontend-module.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 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 { AIVariableContribution } from '@theia/ai-core';
|
||||
import { FrontendApplicationContribution, KeybindingContribution } from '@theia/core/lib/browser';
|
||||
import { CommandContribution, MenuContribution } from '@theia/core/lib/common';
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import '../../style/ask-ai-input.css';
|
||||
import { AICodeActionProvider } from './ai-code-action-provider';
|
||||
import { AiEditorCommandContribution } from './ai-editor-command-contribution';
|
||||
import { EditorContextVariableContribution } from './ai-editor-context-variable';
|
||||
import {
|
||||
AskAIInputArgs,
|
||||
AskAIInputConfiguration,
|
||||
AskAIInputFactory,
|
||||
AskAIInputWidget
|
||||
} from './ask-ai-input-widget';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(AiEditorCommandContribution).toSelf().inSingletonScope();
|
||||
|
||||
bind(CommandContribution).toService(AiEditorCommandContribution);
|
||||
bind(MenuContribution).toService(AiEditorCommandContribution);
|
||||
bind(KeybindingContribution).toService(AiEditorCommandContribution);
|
||||
|
||||
bind(AIVariableContribution).to(EditorContextVariableContribution).inSingletonScope();
|
||||
|
||||
bind(AICodeActionProvider).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(AICodeActionProvider);
|
||||
|
||||
bind(AskAIInputFactory).toFactory(ctx => (args: AskAIInputArgs) => {
|
||||
const container = ctx.container.createChild();
|
||||
container.bind(AskAIInputArgs).toConstantValue(args);
|
||||
container.bind(AskAIInputConfiguration).toConstantValue({
|
||||
showContext: true,
|
||||
showPinnedAgent: true,
|
||||
showChangeSet: false,
|
||||
showSuggestions: false
|
||||
} satisfies AskAIInputConfiguration);
|
||||
container.bind(AskAIInputWidget).toSelf().inSingletonScope();
|
||||
return container.get(AskAIInputWidget);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 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 { ChatRequest } from '@theia/ai-chat';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { Emitter, Event } from '@theia/core/lib/common/event';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { MonacoEditorZoneWidget } from '@theia/monaco/lib/browser/monaco-editor-zone-widget';
|
||||
import { AskAIInputFactory, AskAIInputWidget } from './ask-ai-input-widget';
|
||||
|
||||
/**
|
||||
* A widget that shows the Ask AI input UI in a Monaco editor zone.
|
||||
*/
|
||||
export class AskAIInputMonacoZoneWidget extends MonacoEditorZoneWidget implements Disposable {
|
||||
protected readonly inputWidget: AskAIInputWidget;
|
||||
|
||||
protected readonly onSubmitEmitter = new Emitter<ChatRequest>();
|
||||
protected readonly onCancelEmitter = new Emitter<void>();
|
||||
|
||||
readonly onSubmit: Event<ChatRequest> = this.onSubmitEmitter.event;
|
||||
readonly onCancel: Event<void> = this.onCancelEmitter.event;
|
||||
|
||||
constructor(
|
||||
editorInstance: monaco.editor.ICodeEditor,
|
||||
inputWidgetFactory: AskAIInputFactory
|
||||
) {
|
||||
super(editorInstance, false /* showArrow */);
|
||||
|
||||
this.containerNode.classList.add('ask-ai-input-monaco-zone-widget');
|
||||
|
||||
this.inputWidget = inputWidgetFactory({
|
||||
onSubmit: event => this.handleSubmit(event),
|
||||
onCancel: () => this.handleCancel()
|
||||
});
|
||||
|
||||
this.inputWidget.onDidResize(() => this.adjustZoneHeight());
|
||||
|
||||
this.toDispose.pushAll([
|
||||
this.onSubmitEmitter,
|
||||
this.onCancelEmitter,
|
||||
this.inputWidget,
|
||||
]);
|
||||
}
|
||||
|
||||
override show(options: MonacoEditorZoneWidget.Options): void {
|
||||
super.show(options);
|
||||
this.renderReactWidget();
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
showAtLine(lineNumber: number): void {
|
||||
const options: MonacoEditorZoneWidget.Options = {
|
||||
afterLineNumber: lineNumber,
|
||||
heightInLines: 5,
|
||||
frameWidth: 1,
|
||||
showFrame: true,
|
||||
};
|
||||
this.show(options);
|
||||
}
|
||||
|
||||
protected renderReactWidget(): void {
|
||||
this.containerNode.append(this.inputWidget.node);
|
||||
this.inputWidget.activate();
|
||||
this.inputWidget.update();
|
||||
}
|
||||
|
||||
protected adjustZoneHeight(): void {
|
||||
if (!this.viewZone) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editorLineHeight = this.editor.getOption(monaco.editor.EditorOption.lineHeight);
|
||||
const zoneWidgetHeight = this.inputWidget.node.parentElement ? this.inputWidget.node.parentElement.scrollHeight : this.inputWidget.node.scrollHeight;
|
||||
|
||||
const requiredLines = Math.max(5, Math.ceil(zoneWidgetHeight / editorLineHeight));
|
||||
|
||||
if (this.viewZone.heightInLines !== requiredLines) {
|
||||
this.layout(requiredLines);
|
||||
}
|
||||
}
|
||||
|
||||
protected handleSubmit(request: ChatRequest): void {
|
||||
this.onSubmitEmitter.fire(request);
|
||||
this.hide();
|
||||
}
|
||||
|
||||
protected handleCancel(): void {
|
||||
this.onCancelEmitter.fire();
|
||||
this.hide();
|
||||
}
|
||||
|
||||
protected registerListeners(): void {
|
||||
this.toHide.push(this.editor.onKeyDown(e => {
|
||||
if (e.keyCode === monaco.KeyCode.Escape) {
|
||||
this.handleCancel();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
93
packages/ai-editor/src/browser/ask-ai-input-widget.ts
Normal file
93
packages/ai-editor/src/browser/ask-ai-input-widget.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 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 { ChatRequest, MutableChatModel } from '@theia/ai-chat';
|
||||
import { AIChatInputConfiguration, AIChatInputWidget } from '@theia/ai-chat-ui/lib/browser/chat-input-widget';
|
||||
import { CHAT_VIEW_LANGUAGE_EXTENSION } from '@theia/ai-chat-ui/lib/browser/chat-view-language-contribution';
|
||||
import { generateUuid, URI } from '@theia/core';
|
||||
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
|
||||
|
||||
export const AskAIInputConfiguration = Symbol('AskAIInputConfiguration');
|
||||
export interface AskAIInputConfiguration extends AIChatInputConfiguration { }
|
||||
|
||||
export const AskAIInputArgs = Symbol('AskAIInputArgs');
|
||||
export interface AskAIInputArgs {
|
||||
onSubmit: (request: ChatRequest) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const AskAIInputFactory = Symbol('AskAIInputFactory');
|
||||
export type AskAIInputFactory = (args: AskAIInputArgs) => AskAIInputWidget;
|
||||
|
||||
/**
|
||||
* React input widget for Ask AI functionality, extending the AIChatInputWidget.
|
||||
*/
|
||||
@injectable()
|
||||
export class AskAIInputWidget extends AIChatInputWidget {
|
||||
public static override ID = 'ask-ai-input-widget';
|
||||
|
||||
@inject(AskAIInputArgs) @optional()
|
||||
protected readonly args: AskAIInputArgs | undefined;
|
||||
|
||||
@inject(AskAIInputConfiguration) @optional()
|
||||
protected override readonly configuration: AskAIInputConfiguration | undefined;
|
||||
|
||||
protected readonly resourceId = generateUuid();
|
||||
protected override heightInLines = 3;
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
|
||||
this.id = AskAIInputWidget.ID;
|
||||
|
||||
const noOp = () => { };
|
||||
// We need to set those values here, otherwise the widget will throw an error
|
||||
this.onUnpin = noOp;
|
||||
this.onCancel = noOp;
|
||||
this.onDeleteChangeSet = noOp;
|
||||
this.onDeleteChangeSetElement = noOp;
|
||||
|
||||
// Create a temporary chat model for the widget
|
||||
this.chatModel = new MutableChatModel();
|
||||
|
||||
this.setEnabled(true);
|
||||
this.onQuery = this.handleSubmit.bind(this);
|
||||
this.onCancel = this.handleCancel.bind(this);
|
||||
}
|
||||
|
||||
protected override getResourceUri(): URI {
|
||||
return new URI(`ask-ai:/input-${this.resourceId}.${CHAT_VIEW_LANGUAGE_EXTENSION}`);
|
||||
}
|
||||
|
||||
protected handleSubmit(query: string): Promise<void> {
|
||||
const userInput = query.trim();
|
||||
if (userInput) {
|
||||
const request: ChatRequest = { text: userInput, variables: this._chatModel.context.getVariables() };
|
||||
|
||||
this.args?.onSubmit(request);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
protected handleCancel(): void {
|
||||
this.args?.onCancel();
|
||||
}
|
||||
|
||||
protected override onEscape(): void {
|
||||
this.handleCancel();
|
||||
}
|
||||
}
|
||||
18
packages/ai-editor/src/browser/index.ts
Normal file
18
packages/ai-editor/src/browser/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 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
|
||||
// *****************************************************************************
|
||||
|
||||
export * from './ai-editor-command-contribution';
|
||||
export * from './ask-ai-input-widget';
|
||||
28
packages/ai-editor/src/package.spec.ts
Normal file
28
packages/ai-editor/src/package.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 EclipseSource GmbH and others.
|
||||
//
|
||||
// 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
|
||||
// *****************************************************************************
|
||||
|
||||
/* note: this bogus test file is required so that
|
||||
we are able to run mocha unit tests on this
|
||||
package, without having any actual unit tests in it.
|
||||
This way a coverage report will be generated,
|
||||
showing 0% coverage, instead of no report.
|
||||
This file can be removed once we have real unit
|
||||
tests in place. */
|
||||
|
||||
describe('ai-editor package', () => {
|
||||
|
||||
it('support code coverage statistics', () => true);
|
||||
});
|
||||
26
packages/ai-editor/style/ask-ai-input.css
Normal file
26
packages/ai-editor/style/ask-ai-input.css
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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
|
||||
*/
|
||||
|
||||
.ask-ai-input-monaco-zone-widget {
|
||||
background-color: var(--theia-editorWidget-background);
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.ask-ai-input-monaco-zone-widget .theia-ChatInput {
|
||||
margin-top: 10px;
|
||||
margin-left: 55px;
|
||||
width: 75%;
|
||||
}
|
||||
37
packages/ai-editor/tsconfig.json
Normal file
37
packages/ai-editor/tsconfig.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../ai-chat"
|
||||
},
|
||||
{
|
||||
"path": "../ai-chat-ui"
|
||||
},
|
||||
{
|
||||
"path": "../ai-core"
|
||||
},
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../editor"
|
||||
},
|
||||
{
|
||||
"path": "../filesystem"
|
||||
},
|
||||
{
|
||||
"path": "../monaco"
|
||||
},
|
||||
{
|
||||
"path": "../workspace"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user