diff --git a/packages/ai-native/src/browser/ai-core.contribution.ts b/packages/ai-native/src/browser/ai-core.contribution.ts index 3c8bc52393..21e925ac46 100644 --- a/packages/ai-native/src/browser/ai-core.contribution.ts +++ b/packages/ai-native/src/browser/ai-core.contribution.ts @@ -204,22 +204,26 @@ export class AINativeBrowserContribution }); registry.registerSettingSection(AI_NATIVE_SETTING_GROUP_ID, { - title: localize('preference.aiNative.chat.title'), + title: localize('preference.ai.native.chat.title'), preferences: [ { id: AINativeSettingSectionsId.CHAT_VISIBLE_TYPE, - localized: 'preference.aiNative.chat.visible.type', + localized: 'preference.ai.native.chat.visible.type', }, ], }); if (this.aiNativeConfigService.capabilities.supportsInlineChat) { registry.registerSettingSection(AI_NATIVE_SETTING_GROUP_ID, { - title: localize('preference.aiNative.inlineChat.title'), + title: localize('preference.ai.native.inlineChat.title'), preferences: [ { id: AINativeSettingSectionsId.INLINE_CHAT_AUTO_VISIBLE, - localized: 'preference.aiNative.inlineChat.auto.visible', + localized: 'preference.ai.native.inlineChat.auto.visible', + }, + { + id: AINativeSettingSectionsId.INLINE_CHAT_CODE_ACTION_ENABLED, + localized: 'preference.ai.native.inlineChat.codeAction.enabled', }, ], }); diff --git a/packages/ai-native/src/browser/ai-editor.contribution.ts b/packages/ai-native/src/browser/ai-editor.contribution.ts index 21b8a886ee..455f5879ce 100644 --- a/packages/ai-native/src/browser/ai-editor.contribution.ts +++ b/packages/ai-native/src/browser/ai-editor.contribution.ts @@ -5,8 +5,10 @@ import { AINativeConfigService, IAIInlineChatService, PreferenceService } from ' import { IBrowserCtxMenu } from '@opensumi/ide-core-browser/lib/menu/next/renderer/ctxmenu/browser'; import { AINativeSettingSectionsId, + AISerivceType, CancelResponse, CancellationToken, + ChatResponse, ContributionProvider, Disposable, ErrorResponse, @@ -21,13 +23,14 @@ import { ReplyResponse, Schemes, SupportLogNamespace, + getErrorMessage, runWhenIdle, } from '@opensumi/ide-core-common'; -import { ChatResponse } from '@opensumi/ide-core-common'; import { DesignBrowserCtxMenuService } from '@opensumi/ide-design/lib/browser/override/menu.service'; import { EditorSelectionChangeEvent, IEditor, IEditorFeatureContribution } from '@opensumi/ide-editor/lib/browser'; import * as monaco from '@opensumi/ide-monaco'; import { monaco as monacoApi } from '@opensumi/ide-monaco/lib/browser/monaco-api'; +import { MonacoTelemetryService } from '@opensumi/ide-monaco/lib/browser/telemetry.service'; import { AIInlineChatContentWidget } from '../common'; @@ -90,6 +93,9 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo @Autowired(LanguageParserService) protected languageParserService: LanguageParserService; + @Autowired() + monacoTelemetryService: MonacoTelemetryService; + private latestMiddlewareCollector: IAIMiddleware; private logger: ILogServiceClient; @@ -131,31 +137,55 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo return this; } + this.contributeInlineCompletionFeature(editor); + this.contributeInlineChatFeature(editor); this.registerLanguageFeatures(editor); this.disposables.push( - this.aiNativeService.onInlineChatVisible((value: boolean) => { - if (value) { - this.showInlineChat(editor); - } else { - this.disposeAllWidget(); - } + monacoEditor.onDidScrollChange(() => { + /** + * 其他的 ctxmenu 服务注册的菜单在 onHide 函数里会有其他逻辑处理,例如在 editor.context.ts 会在 hide 的时候 focus 编辑器,影响使用 + */ + this.ctxMenuRenderer.onHide = undefined; + this.ctxMenuRenderer.hide(true); }), ); - let needShowInlineChat = false; + return this; + } + + protected contributeInlineCompletionFeature(editor: IEditor): void { + const { monacoEditor } = editor; + // 判断用户是否选择了一块区域或者移动光标 取消掉请补全求 + const selectionChange = () => { + this.aiCompletionsService.hideStatusBarItem(); + const selection = monacoEditor.getSelection(); + if (!selection) { + return; + } + + // 判断是否选中区域 + if (selection.startLineNumber !== selection.endLineNumber || selection.startColumn !== selection.endColumn) { + this.aiInlineCompletionsProvider.cancelRequest(); + } + requestAnimationFrame(() => { + this.aiCompletionsService.setVisibleCompletion(false); + }); + }; + + const debouncedSelectionChange = debounce(selectionChange, 50, { + maxWait: 200, + leading: true, + trailing: true, + }); this.disposables.push( - monacoEditor.onMouseDown(() => { - needShowInlineChat = false; - }), - monacoEditor.onMouseUp((event) => { - const target = event.target; - const detail = (target as any).detail; - if (detail && typeof detail === 'string' && detail === AIInlineChatContentWidget) { - needShowInlineChat = false; + this.eventBus.on(EditorSelectionChangeEvent, (e) => { + if (e.payload.source === 'mouse') { + debouncedSelectionChange(); } else { - needShowInlineChat = true; + debouncedSelectionChange.cancel(); + selectionChange(); } }), monacoEditor.onDidChangeModelContent((e) => { @@ -177,12 +207,43 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo this.aiCompletionsService.hideStatusBarItem(); this.aiCompletionsService.setVisibleCompletion(false); }), - monacoEditor.onDidScrollChange(() => { - /** - * 其他的 ctxmenu 服务注册的菜单在 onHide 函数里会有其他逻辑处理,例如在 editor.context.ts 会在 hide 的时候 focus 编辑器,影响使用 - */ - this.ctxMenuRenderer.onHide = undefined; - this.ctxMenuRenderer.hide(true); + ); + } + + protected contributeInlineChatFeature(editor: IEditor): void { + const { monacoEditor } = editor; + + this.disposables.push( + this.aiNativeService.onInlineChatVisible((value: boolean) => { + if (value) { + this.showInlineChat(editor); + } else { + this.disposeAllWidget(); + } + }), + // 通过 code actions 来透出我们 inline chat 的功能 + this.inlineChatFeatureRegistry.onCodeActionRun(({ id, range }) => { + monacoEditor.setSelection(range); + this.showInlineChat(editor); + if (this.aiInlineContentWidget) { + this.aiInlineContentWidget.clickActionId(id, 'codeAction'); + } + }), + ); + + let needShowInlineChat = false; + this.disposables.push( + monacoEditor.onMouseDown(() => { + needShowInlineChat = false; + }), + monacoEditor.onMouseUp((event) => { + const target = event.target; + const detail = (target as any).detail; + if (detail && typeof detail === 'string' && detail === AIInlineChatContentWidget) { + needShowInlineChat = false; + } else { + needShowInlineChat = true; + } }), ); @@ -190,16 +251,14 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo AINativeSettingSectionsId.INLINE_CHAT_AUTO_VISIBLE, true, ); - this.disposables.push({ - dispose: () => { - this.preferenceService.onSpecificPreferenceChange( - AINativeSettingSectionsId.INLINE_CHAT_AUTO_VISIBLE, - ({ newValue }) => { - prefInlineChatAutoVisible = newValue; - }, - ); - }, - }); + this.disposables.push( + this.preferenceService.onSpecificPreferenceChange( + AINativeSettingSectionsId.INLINE_CHAT_AUTO_VISIBLE, + ({ newValue }) => { + prefInlineChatAutoVisible = newValue; + }, + ), + ); this.disposables.push( Event.debounce( @@ -207,11 +266,7 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo (_, e) => e, 100, )(() => { - if (!prefInlineChatAutoVisible) { - return; - } - - if (!needShowInlineChat) { + if (!prefInlineChatAutoVisible || !needShowInlineChat) { return; } @@ -225,42 +280,10 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo this.showInlineChat(editor); }), ); - - // 判断用户是否选择了一块区域或者移动光标 取消掉请补全求 - const selectionChange = () => { - this.aiCompletionsService.hideStatusBarItem(); - const selection = monacoEditor.getSelection()!; - // 判断是否选中区域 - if (selection.startLineNumber !== selection.endLineNumber || selection.startColumn !== selection.endColumn) { - this.aiInlineCompletionsProvider.cancelRequest(); - } - requestAnimationFrame(() => { - this.aiCompletionsService.setVisibleCompletion(false); - }); - }; - - const debouncedSelectionChange = debounce(() => selectionChange(), 50, { - maxWait: 200, - leading: true, - trailing: true, - }); - - this.disposables.push( - this.eventBus.on(EditorSelectionChangeEvent, (e) => { - if (e.payload.source === 'mouse') { - debouncedSelectionChange(); - } else { - debouncedSelectionChange.cancel(); - selectionChange(); - } - }), - ); - - return this; } protected inlineChatInUsing = false; - private async showInlineChat(editor: IEditor, actionId?: string): Promise { + protected async showInlineChat(editor: IEditor): Promise { if (!this.aiNativeConfigService.capabilities.supportsInlineChat) { return; } @@ -272,11 +295,7 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo this.disposeAllWidget(); - const { monacoEditor, currentUri } = editor; - - if (!currentUri || currentUri.codeUri.scheme !== Schemes.file) { - return; - } + const { monacoEditor } = editor; const selection = monacoEditor.getSelection(); @@ -285,7 +304,7 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo return; } - this.aiInlineChatService.launchChatStatus(EInlineChatStatus.READY); + this.aiInlineChatDisposed.addDispose(this.aiInlineChatService.launchChatStatus(EInlineChatStatus.READY)); this.aiInlineContentWidget = this.injector.get(AIInlineContentWidget, [monacoEditor]); @@ -294,16 +313,22 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo }); this.aiInlineChatDisposed.addDispose( - this.aiInlineContentWidget.onActionClick((id: string) => { - this.runInlineChatAction(id, monacoEditor); + this.aiInlineContentWidget.onActionClick((action) => { + this.runInlineChatAction(action, monacoEditor); }), ); - if (actionId) { - this.aiInlineContentWidget.clickActionId(actionId); - } } - private async runInlineChatAction(id: string, monacoEditor: monaco.ICodeEditor) { + private async runInlineChatAction( + { + actionId: id, + source, + }: { + actionId: string; + source: string; + }, + monacoEditor: monaco.ICodeEditor, + ) { const handler = this.inlineChatFeatureRegistry.getEditorHandler(id); const action = this.inlineChatFeatureRegistry.getAction(id); if (!handler || !action) { @@ -328,7 +353,12 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo .setStartPosition(selection.startLineNumber, 1) .setEndPosition(selection.endLineNumber, Number.MAX_SAFE_INTEGER); - const relationId = this.aiReporter.start(action.name, { message: action.name }); + const relationId = this.aiReporter.start(action.name, { + message: action.name, + type: AISerivceType.InlineChat, + source, + runByCodeAction: source === 'codeAction', + }); const result = await this.handleDiffPreviewStrategy( monacoEditor, @@ -338,14 +368,11 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo false, ); - this.aiInlineChatDisposed.addDispose( + this.aiInlineChatDisposed.addDispose([ this.aiInlineChatService.onDiscard(() => { this.aiReporter.end(relationId, { message: result.message, success: true, isDrop: true }); this.disposeAllWidget(); }), - ); - - this.aiInlineChatDisposed.addDispose( this.aiInlineChatService.onRegenerate(async () => { await this.handleDiffPreviewStrategy( monacoEditor, @@ -355,7 +382,7 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo true, ); }), - ); + ]); } } @@ -374,13 +401,13 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo this.resetDiffEnvironment(); const crossCode = model!.getValueInRange(crossSelection); - this.aiInlineChatService.launchChatStatus(EInlineChatStatus.THINKING); + this.aiInlineChatDisposed.addDispose(this.aiInlineChatService.launchChatStatus(EInlineChatStatus.THINKING)); const startTime = Date.now(); const response = await strategy(monacoEditor, this.aiNativeService.cancelIndicator.token); if (this.aiInlineChatDisposed.disposed || CancelResponse.is(response)) { - this.aiInlineChatService.launchChatStatus(EInlineChatStatus.READY); + this.aiInlineChatDisposed.addDispose(this.aiInlineChatService.launchChatStatus(EInlineChatStatus.READY)); this.aiReporter.end(relationId, { message: response.message, success: true, @@ -392,7 +419,7 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo } if (ErrorResponse.is(response)) { - this.aiInlineChatService.launchChatStatus(EInlineChatStatus.ERROR); + this.aiInlineChatDisposed.addDispose(this.aiInlineChatService.launchChatStatus(EInlineChatStatus.ERROR)); this.aiReporter.end(relationId, { message: response.message, success: false, @@ -402,7 +429,7 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo return response; } - this.aiInlineChatService.launchChatStatus(EInlineChatStatus.DONE); + this.aiInlineChatDisposed.addDispose(this.aiInlineChatService.launchChatStatus(EInlineChatStatus.DONE)); this.aiReporter.end(relationId, { message: response.message, @@ -483,11 +510,7 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo } private async registerLanguageFeatures(editor: IEditor): Promise { - const { monacoEditor, currentUri } = editor; - - if (currentUri && currentUri.codeUri.scheme !== Schemes.file) { - return; - } + const { monacoEditor } = editor; this.disposables.push( Event.debounce( @@ -506,6 +529,8 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo this.modelSessionDisposable = new Disposable(); + const languageId = model.getLanguageId(); + if (this.aiNativeConfigService.capabilities.supportsInlineCompletion) { this.contributions.getContributions().forEach((contribution) => { if (contribution.middleware) { @@ -520,7 +545,7 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo }, }); this.modelSessionDisposable.addDispose( - monacoApi.languages.registerInlineCompletionsProvider(model.getLanguageId(), { + monacoApi.languages.registerInlineCompletionsProvider(languageId, { provideInlineCompletions: async (model, position, context, token) => { if (this.latestMiddlewareCollector?.language?.provideInlineCompletions) { this.aiCompletionsService.setMiddlewareComplete( @@ -543,7 +568,7 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo return list; }, freeInlineCompletions() {}, - handleItemDidShow: (completions, item) => { + handleItemDidShow: (completions) => { if (completions.items.length > 0) { this.aiCompletionsService.setVisibleCompletion(true); } @@ -553,103 +578,204 @@ export class AIEditorContribution extends Disposable implements IEditorFeatureCo } if (this.aiNativeConfigService.capabilities.supportsRenameSuggestions) { - const provider = async (model: monaco.ITextModel, range: monaco.IRange, token: CancellationToken) => { - const result = await this.renameSuggestionService.provideRenameSuggestions(model, range, token); - return result; - }; - - this.modelSessionDisposable.addDispose( - monacoApi.languages.registerNewSymbolNameProvider(model.getLanguageId(), { - provideNewSymbolNames: provider, - }), - ); + this.modelSessionDisposable.addDispose(this.contributeRenameFeature(languageId)); } if (this.aiNativeConfigService.capabilities.supportsInlineChat) { - // 通过 code actions 来透出我们 inline chat 的功能 - const languageId = model.getLanguageId(); - this.modelSessionDisposable.addDispose( - this.inlineChatFeatureRegistry.onActionRun(({ id, range }) => { - monacoEditor.setSelection(range); - this.showInlineChat(editor, id); - }), - ); - this.modelSessionDisposable.addDispose( - monacoApi.languages.registerCodeActionProvider(languageId, { - provideCodeActions: async (model) => { - const parser = this.languageParserService.createParser(languageId); - if (!parser) { - return; - } - const actions = this.inlineChatFeatureRegistry.getCodeActions(); - if (!actions || actions.length === 0) { - return; - } + this.modelSessionDisposable.addDispose(this.contributeCodeActionFeature(languageId, editor)); + } + }), + ); + } - const cursorPosition = monacoEditor.getPosition(); - if (!cursorPosition) { - return; - } + lastModelRequestRenameEndTime: number | undefined; + lastModelRequestRenameSessionId: string | undefined; - function constructCodeActions(info: ICodeBlockInfo) { - return { - actions: actions.map((v) => { - const command = {} as monaco.Command; - if (v.command) { - command.id = v.command.id; - command.arguments = [info.range]; - } - - let title = v.title; - - switch (info.infoCategory) { - case 'function': { - title = title + ` for Function: ${info.name}`; - } - } - - return { - ...v, - title, - ranges: [info.range], - command, - }; - }) as monaco.CodeAction[], - dispose() {}, - }; - } + protected contributeRenameFeature(languageId: string): IDisposable { + const disposable = new Disposable(); - const info = await parser.provideCodeBlockInfo(model, cursorPosition); + const provider = async (model: monaco.ITextModel, range: monaco.IRange, token: CancellationToken) => { + this.lastModelRequestRenameSessionId = undefined; - if (info) { - return constructCodeActions(info); - } + const startTime = +new Date(); + const relationId = this.aiReporter.start('rename', { + message: 'start', + type: AISerivceType.Rename, + modelRequestStartTime: startTime, + }); + this.lastModelRequestRenameSessionId = relationId; - // check current line is empty - const currentLineLength = model.getLineLength(cursorPosition.lineNumber); - if (currentLineLength !== 0) { - return; - } + const toDispose = token.onCancellationRequested(() => { + const endTime = +new Date(); - // 获取视窗范围内的代码块 - const range = monacoEditor.getVisibleRanges(); - if (range.length === 0) { - return; - } + this.aiReporter.end(relationId, { + message: 'cancel', + success: false, + isCancel: true, + modelRequestStartTime: startTime, + modelRequestEndTime: endTime, + }); - // 查找从当前行至视窗最后一行的代码块中是否包含函数 - const newRange = new monaco.Range(cursorPosition.lineNumber, 0, range[0].endLineNumber + 1, 0); + this.lastModelRequestRenameSessionId = undefined; + }); - const rangeInfo = await parser.provideCodeBlockInfoInRange(model, newRange); - if (rangeInfo) { - return constructCodeActions(rangeInfo); - } - }, - }), - ); + try { + const result = await this.renameSuggestionService.provideRenameSuggestions(model, range, token); + toDispose.dispose(); + this.lastModelRequestRenameEndTime = +new Date(); + return result; + } catch (error) { + const endTime = +new Date(); + this.aiReporter.end(relationId, { + message: 'error:' + getErrorMessage(error), + success: false, + modelRequestStartTime: startTime, + modelRequestEndTime: endTime, + }); + throw error; + } + }; + + disposable.addDispose([ + monacoApi.languages.registerNewSymbolNameProvider(languageId, { + provideNewSymbolNames: provider, + }), + this.monacoTelemetryService.onEventLog('renameInvokedEvent', (event) => { + if (this.lastModelRequestRenameSessionId) { + this.aiReporter.end(this.lastModelRequestRenameSessionId, { + message: 'done', + success: true, + modelRequestEndTime: this.lastModelRequestRenameEndTime, + ...event, + }); } }), + ]); + + return disposable; + } + + protected contributeCodeActionFeature(languageId: string, editor: IEditor): IDisposable { + const disposable = new Disposable(); + + let prefInlineChatActionEnabled = this.preferenceService.getValid( + AINativeSettingSectionsId.INLINE_CHAT_CODE_ACTION_ENABLED, + true, ); + + if (!prefInlineChatActionEnabled) { + return disposable; + } + + const { monacoEditor } = editor; + const { languageParserService, inlineChatFeatureRegistry } = this; + + let codeActionDispose: IDisposable | undefined; + + disposable.addDispose( + this.preferenceService.onSpecificPreferenceChange( + AINativeSettingSectionsId.INLINE_CHAT_CODE_ACTION_ENABLED, + ({ newValue }) => { + prefInlineChatActionEnabled = newValue; + if (newValue) { + register(); + } else { + if (codeActionDispose) { + codeActionDispose.dispose(); + codeActionDispose = undefined; + } + } + }, + ), + ); + + register(); + + return disposable; + + function register() { + if (codeActionDispose) { + codeActionDispose.dispose(); + codeActionDispose = undefined; + } + + codeActionDispose = monacoApi.languages.registerCodeActionProvider(languageId, { + provideCodeActions: async (model) => { + if (!prefInlineChatActionEnabled) { + return; + } + + const parser = languageParserService.createParser(languageId); + if (!parser) { + return; + } + const actions = inlineChatFeatureRegistry.getCodeActions(); + if (!actions || actions.length === 0) { + return; + } + + const cursorPosition = monacoEditor.getPosition(); + if (!cursorPosition) { + return; + } + + function constructCodeActions(info: ICodeBlockInfo) { + return { + actions: actions.map((v) => { + const command = {} as monaco.Command; + if (v.command) { + command.id = v.command.id; + command.arguments = [info.range]; + } + + let title = v.title; + + switch (info.infoCategory) { + case 'function': { + title = title + ` for Function: ${info.name}`; + } + } + + return { + ...v, + title, + ranges: [info.range], + command, + }; + }) as monaco.CodeAction[], + dispose() {}, + }; + } + + const info = await parser.provideCodeBlockInfo(model, cursorPosition); + if (info) { + return constructCodeActions(info); + } + + // check current line is empty + const currentLineLength = model.getLineLength(cursorPosition.lineNumber); + if (currentLineLength !== 0) { + return; + } + + // 获取视窗范围内的代码块 + const ranges = monacoEditor.getVisibleRanges(); + if (ranges.length === 0) { + return; + } + + // 查找从当前行至视窗最后一行的代码块中是否包含函数 + const newRange = new monaco.Range(cursorPosition.lineNumber, 0, ranges[0].endLineNumber + 1, 0); + + const rangeInfo = await parser.provideCodeBlockInfoInRange(model, newRange); + if (rangeInfo) { + return constructCodeActions(rangeInfo); + } + }, + }); + + disposable.addDispose(codeActionDispose); + } } dispose(): void { diff --git a/packages/ai-native/src/browser/widget/inline-chat/inline-chat-controller.tsx b/packages/ai-native/src/browser/widget/inline-chat/inline-chat-controller.tsx index 4de1ae3a27..b62278b4bb 100644 --- a/packages/ai-native/src/browser/widget/inline-chat/inline-chat-controller.tsx +++ b/packages/ai-native/src/browser/widget/inline-chat/inline-chat-controller.tsx @@ -4,7 +4,7 @@ import { IAIInlineChatService, useInjectable } from '@opensumi/ide-core-browser' import { AIAction, AIInlineResult, EnhancePopover } from '@opensumi/ide-core-browser/lib/components/ai-native'; import { ContentWidgetContainerPanel } from '@opensumi/ide-core-browser/lib/components/ai-native/content-widget/containerPanel'; import { MenuNode } from '@opensumi/ide-core-browser/lib/menu/next/base'; -import { Emitter, InlineChatFeatureRegistryToken, localize } from '@opensumi/ide-core-common'; +import { InlineChatFeatureRegistryToken, localize } from '@opensumi/ide-core-common'; import { Loading } from '../../components/Loading'; @@ -67,7 +67,7 @@ const AIInlineOperation = (props: IAIInlineOperationProps) => { }; export interface IAIInlineChatControllerProps { - onClickActions: Emitter; + onClickActions: (id: string) => void; onClose?: () => void; } @@ -128,7 +128,7 @@ export const AIInlineChatController = (props: IAIInlineChatControllerProps) => { const handleClickActions = useCallback( (id: string) => { if (onClickActions) { - onClickActions.fire(id); + onClickActions(id); } }, [onClickActions], diff --git a/packages/ai-native/src/browser/widget/inline-chat/inline-chat.feature.registry.ts b/packages/ai-native/src/browser/widget/inline-chat/inline-chat.feature.registry.ts index 5fd7551735..378b4777f7 100644 --- a/packages/ai-native/src/browser/widget/inline-chat/inline-chat.feature.registry.ts +++ b/packages/ai-native/src/browser/widget/inline-chat/inline-chat.feature.registry.ts @@ -36,11 +36,11 @@ export class InlineChatFeatureRegistry extends Disposable implements IInlineChat this.editorHandlerMap.clear(); } - private readonly _onActionRun = new Emitter<{ + private readonly _onCodeActionRun = new Emitter<{ id: string; range: IRange; }>(); - public readonly onActionRun = this._onActionRun.event; + public readonly onCodeActionRun = this._onCodeActionRun.event; static getCommandId(type: 'editor' | 'terminal', id: string) { return `ai-native.inline-chat.${type}.${id}`; @@ -61,7 +61,7 @@ export class InlineChatFeatureRegistry extends Disposable implements IInlineChat }, { execute: async (range: IRange) => { - this._onActionRun.fire({ + this._onCodeActionRun.fire({ id, range, }); diff --git a/packages/ai-native/src/browser/widget/inline-chat/inline-chat.service.ts b/packages/ai-native/src/browser/widget/inline-chat/inline-chat.service.ts index ecda24db8f..a4b004230a 100644 --- a/packages/ai-native/src/browser/widget/inline-chat/inline-chat.service.ts +++ b/packages/ai-native/src/browser/widget/inline-chat/inline-chat.service.ts @@ -44,7 +44,7 @@ export class AIInlineChatService implements IAIInlineChatService { } public launchChatStatus(status: EInlineChatStatus) { - runWhenIdle(() => { + return runWhenIdle(() => { this._status = status; this._onChatStatus.fire(status); }); diff --git a/packages/ai-native/src/browser/widget/inline-chat/inline-content-widget.tsx b/packages/ai-native/src/browser/widget/inline-chat/inline-content-widget.tsx index 90c6875f4e..6c3835574d 100644 --- a/packages/ai-native/src/browser/widget/inline-chat/inline-content-widget.tsx +++ b/packages/ai-native/src/browser/widget/inline-chat/inline-content-widget.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Autowired, INJECTOR_TOKEN, Injectable, Injector } from '@opensumi/di'; import { IAIInlineChatService } from '@opensumi/ide-core-browser'; -import { Emitter, Event } from '@opensumi/ide-core-common'; +import { Emitter } from '@opensumi/ide-core-common'; import * as monaco from '@opensumi/ide-monaco'; import { monacoBrowser } from '@opensumi/ide-monaco/lib/browser'; import { @@ -30,8 +30,11 @@ export class AIInlineContentWidget extends BaseInlineContentWidget { private originTop = 0; - private readonly _onActionClickEmitter = new Emitter(); - public readonly onActionClick: Event = this._onActionClickEmitter.event; + private readonly _onActionClickEmitter = new Emitter<{ + actionId: string; + source: string; + }>(); + public readonly onActionClick = this._onActionClickEmitter.event; constructor(protected readonly editor: IMonacoCodeEditor) { super(editor); @@ -51,12 +54,17 @@ export class AIInlineContentWidget extends BaseInlineContentWidget { super.dispose(); } - clickActionId(actionId: string): void { - this._onActionClickEmitter.fire(actionId); + clickActionId(actionId: string, source: string): void { + this._onActionClickEmitter.fire({ actionId, source }); } public renderView(): React.ReactNode { - return this.dispose()} />; + return ( + this.clickActionId(id, 'widget')} + onClose={() => this.dispose()} + /> + ); } override async show(options?: ShowAIContentOptions | undefined): Promise { @@ -72,7 +80,7 @@ export class AIInlineContentWidget extends BaseInlineContentWidget { return domNode; } - override async hide(options?: ShowAIContentOptions | undefined): Promise { + override async hide(): Promise { this.aiNativeContextKey.inlineChatIsVisible.set(false); super.hide(); } diff --git a/packages/core-browser/src/core-preferences.ts b/packages/core-browser/src/core-preferences.ts index 934ea14782..c5e166c0ca 100644 --- a/packages/core-browser/src/core-preferences.ts +++ b/packages/core-browser/src/core-preferences.ts @@ -301,6 +301,10 @@ export const corePreferenceSchema: PreferenceSchema = { type: 'boolean', default: true, }, + [AINativeSettingSectionsId.INLINE_CHAT_CODE_ACTION_ENABLED]: { + type: 'boolean', + default: true, + }, [AINativeSettingSectionsId.CHAT_VISIBLE_TYPE]: { type: 'string', enum: ['never', 'always', 'default'], diff --git a/packages/core-browser/src/monaco/index.ts b/packages/core-browser/src/monaco/index.ts index f217390076..1638be676b 100644 --- a/packages/core-browser/src/monaco/index.ts +++ b/packages/core-browser/src/monaco/index.ts @@ -31,6 +31,7 @@ export enum ServiceNames { CONTEXT_KEY_SERVICE = 'contextKeyService', BULK_EDIT_SERVICE = 'IWorkspaceEditService', OPENER_SERVICE = 'openerService', + TELEMETRY_SERVICE = 'telemetryService', } export abstract class MonacoService { diff --git a/packages/core-common/src/settings/ai-native.ts b/packages/core-common/src/settings/ai-native.ts index 52b32b34a6..5fe32d6470 100644 --- a/packages/core-common/src/settings/ai-native.ts +++ b/packages/core-common/src/settings/ai-native.ts @@ -1,5 +1,6 @@ export enum AINativeSettingSectionsId { INLINE_CHAT_AUTO_VISIBLE = 'ai.native.inlineChat.auto.visible', + INLINE_CHAT_CODE_ACTION_ENABLED = 'ai.native.inlineChat.codeAction.enabled', CHAT_VISIBLE_TYPE = 'ai.native.chat.visible.type', } export const AI_NATIVE_SETTING_GROUP_ID = 'AI-Native'; diff --git a/packages/core-common/src/types/ai-native/reporter.ts b/packages/core-common/src/types/ai-native/reporter.ts index c5460b6835..f20f1d2216 100644 --- a/packages/core-common/src/types/ai-native/reporter.ts +++ b/packages/core-common/src/types/ai-native/reporter.ts @@ -2,10 +2,12 @@ export const AI_REPORTER_NAME = 'AI'; export enum AISerivceType { Chat = 'chat', + InlineChat = 'inlineChat', CustomReplay = 'customReplay', Completion = 'completion', Agent = 'agent', MergeConflict = 'mergeConflict', + Rename = 'rename', } export interface CommonLogInfo { @@ -68,10 +70,39 @@ export interface MergeConflictRT extends Partial { cancelNum: number; } +export interface RenameRT extends Partial { + /** + * 用户取消了重命名操作 + */ + isCancel?: boolean; + /** + * 开始请求重命名候选项的时间 + */ + modelRequestStartTime: number; + /** + * 请求重命名候选项结束的时间 + */ + modelRequestEndTime: number; +} + +export interface InlineChatRT extends Partial { + /** + * 用户触发 Inline Chat 的来源 + */ + source: string; + + /** + * @deprecated Please use `source` instead + */ + runByCodeAction?: boolean; +} + export type ReportInfo = | Partial | ({ type: AISerivceType.Completion } & CompletionRT) - | ({ type: AISerivceType.MergeConflict } & MergeConflictRT); + | ({ type: AISerivceType.MergeConflict } & MergeConflictRT) + | ({ type: AISerivceType.Rename } & RenameRT) + | ({ type: AISerivceType.InlineChat } & InlineChatRT); export const IAIReporter = Symbol('IAIReporter'); diff --git a/packages/extension/__tests__/browser/extension-service/extension-service-mock-helper.ts b/packages/extension/__tests__/browser/extension-service/extension-service-mock-helper.ts index fa41540d28..2293787841 100644 --- a/packages/extension/__tests__/browser/extension-service/extension-service-mock-helper.ts +++ b/packages/extension/__tests__/browser/extension-service/extension-service-mock-helper.ts @@ -14,6 +14,7 @@ import { CommandRegistry, CommandRegistryImpl, DefaultStorageProvider, + Deferred, Disposable, Emitter, IContextKeyService, @@ -23,8 +24,10 @@ import { IJSONSchemaRegistry, IPreferenceSettingsService, ISchemaStore, + IScopedContextKeyService, KeybindingRegistry, KeybindingRegistryImpl, + MaybeNull, PreferenceProvider, StorageProvider, StorageResolverContribution, @@ -38,7 +41,14 @@ import { IMenuRegistry, MenuRegistryImpl } from '@opensumi/ide-core-browser/lib/ import { StaticResourceService } from '@opensumi/ide-core-browser/lib/static-resource'; import { createBrowserInjector } from '@opensumi/ide-dev-tool/src/injector-helper'; import { mockService } from '@opensumi/ide-dev-tool/src/mock-injector'; -import { WorkbenchEditorService } from '@opensumi/ide-editor'; +import { + IEditor, + IEditorGroup, + IOpenResourceResult, + IResource, + IUntitledOptions, + WorkbenchEditorService, +} from '@opensumi/ide-editor'; import { EditorComponentRegistry, IEditorActionRegistry, @@ -298,12 +308,46 @@ const mockExtensionProps: IExtensionProps = { }; @Injectable() -class MockWorkbenchEditorService { - open() {} - apply() {} - editorGroups = []; +class MockWorkbenchEditorService implements WorkbenchEditorService { + onCursorChange = () => Disposable.NULL; + onDidEditorGroupsChanged = () => Disposable.NULL; + onDidCurrentEditorGroupChanged = () => Disposable.NULL; onActiveResourceChange = () => Disposable.NULL; onActiveEditorUriChange = () => Disposable.NULL; + + contributionsReady = new Deferred(); + + editorGroups = []; + sortedEditorGroups: IEditorGroup[]; + currentEditor: IEditor | null; + currentOrPreviousFocusedEditor: IEditor | null; + currentResource: MaybeNull>; + currentEditorGroup: IEditorGroup; + closeAll(uri?: URI | undefined, force?: boolean | undefined): Promise { + throw new Error('Method not implemented.'); + } + openUris(uri: URI[]): Promise { + throw new Error('Method not implemented.'); + } + saveAll(includeUntitled?: boolean | undefined): Promise { + throw new Error('Method not implemented.'); + } + close(uri: any, force?: boolean | undefined): Promise { + throw new Error('Method not implemented.'); + } + getAllOpenedUris(): URI[] { + throw new Error('Method not implemented.'); + } + createUntitledResource(options?: IUntitledOptions | undefined): Promise { + throw new Error('Method not implemented.'); + } + setEditorContextKeyService(contextKeyService: IScopedContextKeyService): void { + throw new Error('Method not implemented.'); + } + async open() { + return {} as any; + } + apply() {} } const mockExtension = { diff --git a/packages/extension/__tests__/browser/extension-service/extension.worker.service.test.ts b/packages/extension/__tests__/browser/extension-service/extension.worker.service.test.ts index 98d6fb7006..888f71332e 100644 --- a/packages/extension/__tests__/browser/extension-service/extension.worker.service.test.ts +++ b/packages/extension/__tests__/browser/extension-service/extension.worker.service.test.ts @@ -1,7 +1,7 @@ import { URI } from '@opensumi/ide-core-browser'; +import { MockInjector } from '@opensumi/ide-dev-tool/src/mock-injector'; import { WorkerExtProcessService } from '@opensumi/ide-extension/lib/browser/extension-worker.service'; -import { MockInjector } from '../../../../../tools/dev-tool/src/mock-injector'; import { IExtensionWorkerHost, WorkerHostAPIIdentifier } from '../../../src/common'; import { MOCK_EXTENSIONS, setupExtensionServiceInjector } from './extension-service-mock-helper'; diff --git a/packages/extension/src/browser/vscode/api/main.thread.api.impl.ts b/packages/extension/src/browser/vscode/api/main.thread.api.impl.ts index 7c289b464b..19e7458e38 100644 --- a/packages/extension/src/browser/vscode/api/main.thread.api.impl.ts +++ b/packages/extension/src/browser/vscode/api/main.thread.api.impl.ts @@ -229,6 +229,7 @@ export async function initWorkerThreadAPIProxy(workerProtocol: IRPCProtocol, inj const MainThreadTreeViewAPI = injector.get(MainThreadTreeView, [workerProtocol, 'worker']); const MainThreadDecorationsAPI = injector.get(MainThreadDecorations, [workerProtocol]); const MainThreadLocalizationAPI = injector.get(MainThreadLocalization, [workerProtocol]); + const MainThreadEditorTabsAPI = injector.get(MainThreadEditorTabsService, [workerProtocol]); workerProtocol.set(MainThreadAPIIdentifier.MainThreadLanguages, MainThreadLanguagesAPI); workerProtocol.set( @@ -269,6 +270,7 @@ export async function initWorkerThreadAPIProxy(workerProtocol: IRPCProtocol, inj MainThreadAPIIdentifier.MainThreadDecorations, MainThreadDecorationsAPI, ); + workerProtocol.set(MainThreadAPIIdentifier.MainThreadEditorTabs, MainThreadEditorTabsAPI); workerProtocol.set(MainThreadAPIIdentifier.MainThreadLocalization, MainThreadLocalizationAPI); // 作用和 node extension service 等同,用来设置 webview resourceRoots await MainThreadWebviewAPI.init(); diff --git a/packages/extension/src/hosted/ext.host.ts b/packages/extension/src/hosted/ext.host.ts index b7b53e29bf..e428b56be2 100644 --- a/packages/extension/src/hosted/ext.host.ts +++ b/packages/extension/src/hosted/ext.host.ts @@ -57,6 +57,7 @@ enum EInternalModule { } const __interceptModule = enumValueToArray(EInternalModule); +const __interceptModuleSet = new Set(__interceptModule); abstract class ApiImplFactory { private apiFactory: any; @@ -159,8 +160,6 @@ export default class ExtensionHostServiceImpl implements IExtensionHostService { this.reporterService = new ReporterService(reporter, { host: REPORT_HOST.EXTENSION, }); - - Error.stackTraceLimit = 100; } /** @@ -315,17 +314,15 @@ export default class ExtensionHostServiceImpl implements IExtensionHostService { const that = this; module._load = function load(request: string, parent: any, isMain: any) { - if (!__interceptModule.some((m) => m === request)) { + if (!__interceptModuleSet.has(request)) { return originalLoad.apply(this, arguments); } - // // 可能存在开发插件时通过 npm link 的方式安装的依赖 // 只通过 parent.filename 查找插件无法兼容这种情况 // 因为 parent.filename 拿到的路径并不在同一个目录下 // 往上递归遍历依赖的模块是否在插件目录下 // 最多只查找 3 层,因为不太可能存在更长的依赖关系 - // const extension = that.lookup(parent, 0); if (!extension) { return; @@ -385,7 +382,7 @@ export default class ExtensionHostServiceImpl implements IExtensionHostService { version: extension.packageJSON?.version, }); - this.logger.error(err.message); + this.logger.error(`extension ${extension.id} throw error`, err.message); } } @@ -406,8 +403,7 @@ export default class ExtensionHostServiceImpl implements IExtensionHostService { const isSumiContributes = this.containsSumiContributes(extension); const modulePath: string = extension.path; - this.logger.debug(`${extension.name} - ${modulePath}`); - + this.logger.debug(`active ${extension.name} from ${modulePath}`); this.logger.debug(`active extension host process by ${modulePath}`); const extendProxy = this.getExtendModuleProxy(extension, isSumiContributes); @@ -522,13 +518,13 @@ export default class ExtensionHostServiceImpl implements IExtensionHostService { }, {}); } + /** + * @example + * "sumiContributes": { + * "viewsProxies": ["ViewComponentID"], + * } + */ private getExtendModuleProxy(extension: IExtensionDescription, isSumiContributes: boolean) { - /** - * @example - * "sumiContributes": { - * "viewsProxies": ["ViewComponentID"], - * } - */ if ( isSumiContributes && extension.packageJSON.sumiContributes && diff --git a/packages/extension/src/hosted/ext.process-base.ts b/packages/extension/src/hosted/ext.process-base.ts index 913b6a9bbc..c97be553a5 100644 --- a/packages/extension/src/hosted/ext.process-base.ts +++ b/packages/extension/src/hosted/ext.process-base.ts @@ -165,18 +165,18 @@ export async function extProcessInit(config: ExtProcessConfig = {}) { } }); - logger?.log('preload.init start'); + logger.log('preload.init start'); await preload.init(); - logger?.log('preload.init end'); + logger.log('preload.init end'); if (process && process.send) { process.send('ready'); process.on('message', async (msg) => { if (msg === 'close') { - logger?.log('preload.close start'); + logger.log('preload.close start'); await preload.close(); - logger?.log('preload.close end'); + logger.log('preload.close end'); if (process && process.send) { process.send('finish'); } @@ -184,7 +184,7 @@ export async function extProcessInit(config: ExtProcessConfig = {}) { }); } } catch (e) { - logger?.error(e); + logger.error(e); } } diff --git a/packages/i18n/src/common/en-US.lang.ts b/packages/i18n/src/common/en-US.lang.ts index 9e9f9260a1..ee80cb51bb 100644 --- a/packages/i18n/src/common/en-US.lang.ts +++ b/packages/i18n/src/common/en-US.lang.ts @@ -1469,11 +1469,12 @@ export const localizationBundle = { 'aiNative.chat.welcome.loading.text': 'Initializing...', - 'preference.aiNative.inlineChat.title': 'Inline Chat', - 'preference.aiNative.chat.title': 'Chat', - 'preference.aiNative.inlineChat.auto.visible': - 'Does Inline Chat automatically appear when code snippets are selected ?', - 'preference.aiNative.chat.visible.type': 'Control how the chat panel is displayed by default', + 'preference.ai.native.inlineChat.title': 'Inline Chat', + 'preference.ai.native.chat.title': 'Chat', + 'preference.ai.native.inlineChat.auto.visible': 'Does Inline Chat automatically appear when code are selected?', + 'preference.ai.native.inlineChat.codeAction.enabled': + 'Does Inline Chat related code actions automatically appear when code are selected?', + 'preference.ai.native.chat.visible.type': 'Control how the chat panel is displayed by default', // #endregion AI Native // #endregion merge editor diff --git a/packages/i18n/src/common/zh-CN.lang.ts b/packages/i18n/src/common/zh-CN.lang.ts index 1ce4143c97..81a4f26b85 100644 --- a/packages/i18n/src/common/zh-CN.lang.ts +++ b/packages/i18n/src/common/zh-CN.lang.ts @@ -1238,10 +1238,12 @@ export const localizationBundle = { 'aiNative.chat.welcome.loading.text': '初始化中...', - 'preference.aiNative.inlineChat.title': 'Inline Chat', - 'preference.aiNative.chat.title': 'Chat', - 'preference.aiNative.inlineChat.auto.visible': '是否在选中代码片段时自动显示 Inline Chat ?', - 'preference.aiNative.chat.visible.type': '控制 Chat 面板默认的展示方式', + 'preference.ai.native.inlineChat.title': 'Inline Chat', + 'preference.ai.native.chat.title': 'Chat', + 'preference.ai.native.inlineChat.auto.visible': '是否在选中代码片段时自动显示 Inline Chat?', + 'preference.ai.native.inlineChat.codeAction.enabled': + '是否启用在选中代码片段时显示 Inline Chat 相关的 Code Actions?', + 'preference.ai.native.chat.visible.type': '控制 Chat 面板默认的展示方式', // #endregion AI Native 'webview.webviewTagUnavailable': '非 Electron 环境不支持 webview 标签,请使用 iframe 标签', diff --git a/packages/monaco/src/browser/ai-native/BaseInlineContentWidget.tsx b/packages/monaco/src/browser/ai-native/BaseInlineContentWidget.tsx index b2012d9797..2ff7c09ace 100644 --- a/packages/monaco/src/browser/ai-native/BaseInlineContentWidget.tsx +++ b/packages/monaco/src/browser/ai-native/BaseInlineContentWidget.tsx @@ -35,11 +35,13 @@ export abstract class BaseInlineContentWidget extends Disposable implements IInl constructor(protected readonly editor: IMonacoCodeEditor) { super(); - runWhenIdle(() => { - this.root = ReactDOMClient.createRoot(this.getDomNode()); - this.root.render({this.renderView()}); - this.layoutContentWidget(); - }); + this.addDispose( + runWhenIdle(() => { + this.root = ReactDOMClient.createRoot(this.getDomNode()); + this.root.render({this.renderView()}); + this.layoutContentWidget(); + }), + ); } public abstract renderView(): React.ReactNode; @@ -50,11 +52,15 @@ export abstract class BaseInlineContentWidget extends Disposable implements IInl super.dispose(); } - async show(options?: ShowAIContentOptions | undefined): Promise { + show(options?: ShowAIContentOptions | undefined): void { if (!options) { return; } + if (this.disposed) { + return; + } + if (this.options && this.options.selection && this.options.selection.equalsRange(options.selection!)) { return; } @@ -63,7 +69,7 @@ export abstract class BaseInlineContentWidget extends Disposable implements IInl this.editor.addContentWidget(this); } - async hide() { + hide() { this.options = undefined; this.editor.removeContentWidget(this); if (this.root) { diff --git a/packages/monaco/src/browser/monaco.contribution.ts b/packages/monaco/src/browser/monaco.contribution.ts index 1b81c23119..24e0e85c5f 100644 --- a/packages/monaco/src/browser/monaco.contribution.ts +++ b/packages/monaco/src/browser/monaco.contribution.ts @@ -88,6 +88,7 @@ import { MonacoMenus } from './monaco-menu'; import { MonacoSnippetSuggestProvider } from './monaco-snippet-suggest-provider'; import { KEY_CODE_MAP } from './monaco.keycode-map'; import { MonacoResolvedKeybinding } from './monaco.resolved-keybinding'; +import { MonacoTelemetryService } from './telemetry.service'; @Domain(ClientAppContribution, CommandContribution, MenuContribution, KeybindingContribution) export class MonacoClientContribution @@ -245,6 +246,11 @@ export class MonacoClientContribution open: (uri) => this.interceptOpen(new URI(uri.toString())), }); this.overrideServicesRegistry.registerOverrideService(ServiceNames.OPENER_SERVICE, monacoOpenerService); + + this.overrideServicesRegistry.registerOverrideService( + ServiceNames.TELEMETRY_SERVICE, + this.injector.get(MonacoTelemetryService), + ); } private patchMonacoInternalServices() { diff --git a/packages/monaco/src/browser/telemetry.service.ts b/packages/monaco/src/browser/telemetry.service.ts new file mode 100644 index 0000000000..81a2c39be4 --- /dev/null +++ b/packages/monaco/src/browser/telemetry.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@opensumi/di'; +import { Dispatcher } from '@opensumi/ide-core-common'; +import { + ITelemetryService, + TelemetryLevel, +} from '@opensumi/monaco-editor-core/esm/vs/platform/telemetry/common/telemetry'; + +@Injectable() +export class MonacoTelemetryService implements ITelemetryService { + declare readonly _serviceBrand: undefined; + readonly telemetryLevel = TelemetryLevel.NONE; + readonly machineId = 'placeholder'; + readonly sqmId = 'placeholder'; + readonly firstSessionDate = 'placeholder'; + readonly sendErrorTelemetry = false; + + private _sessionId = 'placeholder'; + + private eventLogEmitter = new Dispatcher(); + + onEventLog(type: 'renameInvokedEvent', listener: (e: any) => any) { + return this.eventLogEmitter.on(type)(listener); + } + + get sessionId(): string { + return this._sessionId; + } + + setEnabled(): void {} + setExperimentProperty(): void {} + publicLog() {} + publicLog2(type: string, event: any) { + switch (type) { + case 'renameInvokedEvent': + this.eventLogEmitter.dispatch(type, event); + break; + default: + // ignore + } + } + publicLogError() {} + publicLogError2() {} +} diff --git a/packages/terminal-next/src/common/pty.ts b/packages/terminal-next/src/common/pty.ts index 4f01d6d8c6..350c00cab5 100644 --- a/packages/terminal-next/src/common/pty.ts +++ b/packages/terminal-next/src/common/pty.ts @@ -572,7 +572,7 @@ export interface IShellLaunchConfig { export interface ICreateTerminalOptions { /** * unique long id - * longId = clientId + '|' + shortId(generate by uuid()) + * longId = clientId + '|' + uuid() */ id?: string; /**