69 |
70 |

71 |
72 |
73 |
CodeFuse IDE 有新版本更新
74 |
CodeFuse IDE {updateInfo.version} 可供下载,您现在的版本是 {process.env.IDE_VERSION}。
75 |
更新日志:
76 |
{
80 | const target = e.target as HTMLAnchorElement;
81 | if (target && target.tagName === 'A' && target.href) {
82 | shell.openExternal(target.href);
83 | e.preventDefault();
84 | }
85 | }}
86 | />
87 |
88 |
89 | {updateState === UpdateState.Downloading ? `正在下载更新 (${progressPercent}%) ...` : ''}
90 | {updateState === UpdateState.Downloaded ? '下载完成,准备重启' : ''}
91 | {updateState === UpdateState.DownloadError ? '下载失败,请稍后重试' : ''}
92 | {updateState === UpdateState.UpdateError ? '更新失败,请稍后重试' : ''}
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | )
102 | }
103 |
--------------------------------------------------------------------------------
/src/core/electron-main/app.ts:
--------------------------------------------------------------------------------
1 | import { app, dialog } from 'electron';
2 | import { Injector } from '@opensumi/di'
3 | import { ElectronMainApp as BaseElectronMainApp, ElectronAppConfig } from '@opensumi/ide-core-electron-main';
4 | import { ILogService } from '@/logger/common'
5 | import { ElectronMainContribution } from './types'
6 | import { isMacintosh } from '@opensumi/ide-core-common';
7 | import { WindowsManager } from './window/windows-manager';
8 |
9 | export class ElectronMainApp {
10 | private injector = new Injector;
11 | private baseApp: BaseElectronMainApp;
12 | private logger: ILogService
13 | private pendingQuit = false;
14 |
15 | constructor(config: ElectronAppConfig) {
16 | this.baseApp = new BaseElectronMainApp({
17 | ...config,
18 | injector: this.injector,
19 | })
20 | this.logger = this.injector.get(ILogService);
21 | for (const contribution of this.contributions) {
22 | if (contribution.onBeforeReady) {
23 | contribution.onBeforeReady();
24 | }
25 | }
26 | }
27 |
28 | get contributions() {
29 | return this.baseApp.contributions as ElectronMainContribution[];
30 | }
31 |
32 | async start() {
33 | this.logger.log('start')
34 | await app.whenReady();
35 | this.registerListenerAfterReady()
36 |
37 | this.logger.log('trigger onWillStart')
38 | await Promise.all(this.contributions.map(contribution => contribution.onWillStart?.()))
39 | this.claimInstance();
40 | this.moveToApplication()
41 |
42 | this.logger.log('trigger onStart')
43 | await Promise.all(this.contributions.map(contribution => contribution.onStart?.()))
44 | }
45 |
46 | private registerListenerAfterReady() {
47 | const handleBeforeQuit = () => {
48 | if (this.pendingQuit) return
49 | this.logger.debug('lifecycle#before-quit')
50 | this.pendingQuit = true;
51 | }
52 | app.on('before-quit', handleBeforeQuit)
53 |
54 | const handleWindowAllClose = () => {
55 | this.logger.debug('lifecycle#window-all-closed')
56 | if (this.pendingQuit || !isMacintosh) {
57 | app.quit();
58 | }
59 | }
60 | app.on('window-all-closed', handleWindowAllClose);
61 |
62 | app.once('will-quit', (e) => {
63 | e.preventDefault()
64 | Promise.allSettled(this.contributions.map(contribution => contribution.onWillQuit?.()))
65 | .finally(() => {
66 | app.removeListener('before-quit', handleBeforeQuit)
67 | app.removeListener('window-all-closed', handleWindowAllClose)
68 | this.logger.debug('lifecycle#will-quit')
69 | setTimeout(() => {
70 | app.quit()
71 | })
72 | })
73 | })
74 | }
75 |
76 | private claimInstance() {
77 | const gotTheLock = app.requestSingleInstanceLock({ pid: process.pid })
78 | this.logger.log('gotTheLock:', gotTheLock, process.pid)
79 | if (!gotTheLock) {
80 | app.exit()
81 | } else {
82 | app.on('second-instance', (_event, argv, workingDirectory, additionalData) => {
83 | this.logger.log('second-instance', argv, workingDirectory, additionalData)
84 | if (isMacintosh) {
85 | app.focus({ steal: true });
86 | }
87 | this.injector.get(WindowsManager).createCodeWindow()
88 | })
89 | }
90 | }
91 |
92 | private moveToApplication() {
93 | if (process.platform !== 'darwin' || !app.isPackaged || app.isInApplicationsFolder()) return
94 | const chosen = dialog.showMessageBoxSync({
95 | type: 'question',
96 | buttons: ['移动', '不移动'],
97 | message: '是否移动到 Applications 目录',
98 | defaultId: 0,
99 | cancelId: 1,
100 | })
101 |
102 | if (chosen !== 0) return
103 |
104 | try {
105 | app.moveToApplicationsFolder({
106 | conflictHandler: (conflictType) => {
107 | if (conflictType === 'existsAndRunning') {
108 | dialog.showMessageBoxSync({
109 | type: 'info',
110 | message: '无法移动到 Applications 目录',
111 | detail:
112 | 'Applications 目录已运行另一个版本的 CodeFuse IDE,请先关闭后重试。',
113 | })
114 | }
115 | return true
116 | },
117 | })
118 | } catch (err: any) {
119 | this.logger.error(`Failed to move to applications folder: ${err?.message}}`)
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/bootstrap-web/browser/common-modules.ts:
--------------------------------------------------------------------------------
1 | import { MainLayoutModule } from '@opensumi/ide-main-layout/lib/browser';
2 | import { MenuBarModule } from '@opensumi/ide-menu-bar/lib/browser';
3 | import { MonacoModule } from '@opensumi/ide-monaco/lib/browser';
4 | import { WorkspaceModule } from '@opensumi/ide-workspace/lib/browser';
5 | import { StatusBarModule } from '@opensumi/ide-status-bar/lib/browser';
6 | import { EditorModule } from '@opensumi/ide-editor/lib/browser';
7 | import { ExplorerModule } from '@opensumi/ide-explorer/lib/browser';
8 | import { FileTreeNextModule } from '@opensumi/ide-file-tree-next/lib/browser';
9 | import { FileServiceClientModule } from '@opensumi/ide-file-service/lib/browser';
10 | import { SearchModule } from '@opensumi/ide-search/lib/browser';
11 | import { FileSchemeModule } from '@opensumi/ide-file-scheme/lib/browser';
12 | import { OutputModule } from '@opensumi/ide-output/lib/browser';
13 | import { QuickOpenModule } from '@opensumi/ide-quick-open/lib/browser';
14 | import { BrowserModule, ClientCommonModule, ConstructorOf } from '@opensumi/ide-core-browser';
15 | import { ThemeModule } from '@opensumi/ide-theme/lib/browser';
16 | import { OpenedEditorModule } from '@opensumi/ide-opened-editor/lib/browser';
17 | import { RemoteOpenerModule } from '@opensumi/ide-remote-opener/lib/browser';
18 | import { OutlineModule } from '@opensumi/ide-outline/lib/browser';
19 | import { PreferencesModule } from '@opensumi/ide-preferences/lib/browser';
20 | import { ToolbarModule } from '@opensumi/ide-toolbar/lib/browser';
21 | import { OverlayModule } from '@opensumi/ide-overlay/lib/browser';
22 | import { ExtensionStorageModule } from '@opensumi/ide-extension-storage/lib/browser';
23 | import { StorageModule } from '@opensumi/ide-storage/lib/browser';
24 | import { SCMModule } from '@opensumi/ide-scm/lib/browser';
25 | import { MarkersModule } from '@opensumi/ide-markers/lib/browser';
26 | import { WebviewModule } from '@opensumi/ide-webview';
27 | import { MarkdownModule } from '@opensumi/ide-markdown';
28 | import { LogModule } from '@opensumi/ide-logs/lib/browser';
29 | import { WorkspaceEditModule } from '@opensumi/ide-workspace-edit/lib/browser';
30 | import { ExtensionModule } from '@opensumi/ide-extension/lib/browser';
31 | import { DecorationModule } from '@opensumi/ide-decoration/lib/browser';
32 | import { DebugModule } from '@opensumi/ide-debug/lib/browser';
33 | import { VariableModule } from '@opensumi/ide-variable/lib/browser';
34 | import { KeymapsModule } from '@opensumi/ide-keymaps/lib/browser';
35 | import { MonacoEnhanceModule } from '@opensumi/ide-monaco-enhance/lib/browser/module';
36 | import { OpenVsxExtensionManagerModule } from '@opensumi/ide-extension-manager/lib/browser';
37 | import { TerminalNextModule } from '@opensumi/ide-terminal-next/lib/browser';
38 | import { CommentsModule } from '@opensumi/ide-comments/lib/browser';
39 | import { ClientAddonModule } from '@opensumi/ide-addons/lib/browser';
40 | import { TaskModule } from '@opensumi/ide-task/lib/browser';
41 | import { TestingModule } from '@opensumi/ide-testing/lib/browser';
42 | import {CoreBrowserModule} from "@/core/browser";
43 | import {DesignModule} from "@opensumi/ide-design/lib/browser";
44 | import {AINativeModule} from "@opensumi/ide-ai-native/lib/browser";
45 | import {AIFeatureModule} from "@/ai/browser";
46 | import {AutoUpdaterModule} from "@/auto-updater/browser";
47 |
48 | export const CommonBrowserModules: ConstructorOf
[] = [
49 | MainLayoutModule,
50 | OverlayModule,
51 | LogModule,
52 | ClientCommonModule,
53 | MenuBarModule,
54 | MonacoModule,
55 | StatusBarModule,
56 | EditorModule,
57 | ExplorerModule,
58 | FileTreeNextModule,
59 | FileServiceClientModule,
60 | SearchModule,
61 | FileSchemeModule,
62 | OutputModule,
63 | QuickOpenModule,
64 | MarkersModule,
65 | ThemeModule,
66 | WorkspaceModule,
67 | ExtensionStorageModule,
68 | StorageModule,
69 | OpenedEditorModule,
70 | OutlineModule,
71 | PreferencesModule,
72 | ToolbarModule,
73 | WebviewModule,
74 | MarkdownModule,
75 | WorkspaceEditModule,
76 | SCMModule,
77 | DecorationModule,
78 | DebugModule,
79 | VariableModule,
80 | KeymapsModule,
81 | TerminalNextModule,
82 | ExtensionModule,
83 | OpenVsxExtensionManagerModule,
84 | MonacoEnhanceModule,
85 | ClientAddonModule,
86 | CommentsModule,
87 | TaskModule,
88 | CoreBrowserModule,
89 | TestingModule, RemoteOpenerModule,
90 | // ai
91 | DesignModule,
92 | AINativeModule,
93 | AIFeatureModule,
94 | AutoUpdaterModule,
95 | ];
96 |
--------------------------------------------------------------------------------
/src/ai/browser/command/command-render.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo } from 'react';
2 |
3 | import { ChatThinking, ChatThinkingResult } from '@opensumi/ide-ai-native/lib/browser/components/ChatThinking';
4 | import { ChatMarkdown } from '@opensumi/ide-ai-native/lib/browser/components/ChatMarkdown';
5 | import { TSlashCommandCustomRender } from '@opensumi/ide-ai-native/lib/browser/types';
6 | import { useInjectable, COMMON_COMMANDS, CommandService } from '@opensumi/ide-core-browser';
7 | import { Button } from '@opensumi/ide-core-browser/lib/components';
8 | import { CommandOpener } from '@opensumi/ide-core-browser/lib/opener/command-opener';
9 | import { IAIBackServiceResponse, URI } from '@opensumi/ide-core-common';
10 | import { AICommandService, ISumiModelResp, ISumiCommandModelResp, ISumiSettingModelResp } from './command.service';
11 |
12 | import styles from './command-render.module.less';
13 |
14 | const AiResponseTips = {
15 | ERROR_RESPONSE: '当前与我互动的人太多,请稍后再试,感谢您的理解与支持',
16 | STOP_IMMEDIATELY: '我先不想了,有需要可以随时问我',
17 | NOTFOUND_COMMAND: '很抱歉,暂时未找到可立即执行的命令。',
18 | NOTFOUND_COMMAND_TIP: '你可以打开命令面板搜索相关操作或者重新提问。'
19 | };
20 |
21 | export const CommandRender: TSlashCommandCustomRender = ({ userMessage }) => {
22 | const aiSumiService = useInjectable(AICommandService);
23 | const opener = useInjectable(CommandOpener);
24 | const commandService = useInjectable(CommandService);
25 |
26 | const [loading, setLoading] = React.useState(false);
27 | const [modelRes, setModelRes] = React.useState>();
28 |
29 | const userInput = useMemo(() => {
30 | return userMessage.replace('/IDE', '').trim();
31 | }, [userMessage]);
32 |
33 | useEffect(() => {
34 | if (!userInput) {
35 | return;
36 | }
37 |
38 | setLoading(true);
39 |
40 | aiSumiService.getModelResp(userInput)
41 | .then((resp) => {
42 | setModelRes(resp);
43 | })
44 | .finally(() => {
45 | setLoading(false);
46 | });
47 | }, [userInput]);
48 |
49 | const excute = useCallback(() => {
50 | if (modelRes && modelRes.data) {
51 | if (type === 'command') {
52 | const modelData = data as ISumiCommandModelResp;
53 | opener.open(URI.parse(`command:${modelData.commandKey}`));
54 | return;
55 | }
56 |
57 | if (type === 'setting') {
58 | const modelData = data as ISumiSettingModelResp;
59 |
60 | commandService.executeCommand(COMMON_COMMANDS.OPEN_PREFERENCES.id, modelData.settingKey);
61 | }
62 | }
63 | }, [modelRes]);
64 |
65 |
66 | const failedText = useMemo(() => {
67 | if (!modelRes) {
68 | return '';
69 | }
70 |
71 | return modelRes.errorCode
72 | ? AiResponseTips.ERROR_RESPONSE
73 | : !modelRes.data
74 | ? AiResponseTips.NOTFOUND_COMMAND
75 | : '';
76 | }, [modelRes]);
77 |
78 | const handleRegenerate = useCallback(() => {
79 | console.log('retry');
80 | }, []);
81 |
82 | if (loading || !modelRes) {
83 | return ;
84 | }
85 |
86 | if (failedText) {
87 | return (
88 |
89 | {failedText === AiResponseTips.NOTFOUND_COMMAND ? (
90 |
91 |
{failedText}
92 |
{AiResponseTips.NOTFOUND_COMMAND_TIP}
93 |
107 |
108 | ) : (
109 | failedText
110 | )}
111 |
112 | );
113 | }
114 |
115 | const { data } = modelRes;
116 | const { type, answer } = data ?? {};
117 |
118 | return (
119 |
120 |
121 |
122 | {type !== 'null' && (
123 |
126 | )}
127 |
128 |
129 | );
130 | };
--------------------------------------------------------------------------------
/src/ai/browser/ai-terminal-debug.service.ts:
--------------------------------------------------------------------------------
1 | import { Autowired, Injectable } from '@opensumi/di';
2 | import { Disposable, URI } from '@opensumi/ide-core-common';
3 | import { IFileServiceClient } from '@opensumi/ide-file-service';
4 | import { IWorkspaceService } from '@opensumi/ide-workspace';
5 |
6 | export enum MatcherType {
7 | base,
8 | npm,
9 | typescript,
10 | node,
11 | shell,
12 | java,
13 | }
14 |
15 | export interface MatchResult {
16 | type: MatcherType;
17 | input?: string;
18 | errorText: string;
19 | operate: 'debug' | 'explain';
20 | }
21 |
22 | @Injectable()
23 | export class AITerminalDebugService extends Disposable {
24 | @Autowired(IFileServiceClient)
25 | private fileServiceClient: IFileServiceClient;
26 |
27 | @Autowired(IWorkspaceService)
28 | workspaceService: IWorkspaceService
29 |
30 | private getMessagePrefix(operate: 'debug' | 'explain') {
31 | return operate === 'debug' ? '分析以下内容' : '解释以下内容';
32 | }
33 |
34 | public async generatePrompt(result: MatchResult) {
35 | switch (result.type) {
36 | case MatcherType.typescript:
37 | return await this.generateTsPrompt(result);
38 | case MatcherType.shell:
39 | return await this.generateShellPrompt(result);
40 | case MatcherType.java:
41 | return await this.generateJavaPrompt(result);
42 | default:
43 | return this.generateBasePrompt(result);
44 | }
45 | }
46 |
47 | public generateBasePrompt(result: MatchResult) {
48 | const message = `${this.getMessagePrefix(result.operate)}:\`\`\`\n${result.errorText}\`\`\``;
49 | const prompt = `在 IDE 中进行研发时,终端输出了一些报错信息,其中可能存在多个报错,需要你分别给出每个报错的解决方案,报错信息如下:\`\`\`\n${result.errorText}\n\`\`\``;
50 |
51 | return { message, prompt };
52 | }
53 |
54 | public async generateTsPrompt(result: MatchResult) {
55 | const message = `${this.getMessagePrefix(result.operate)}:\`\`\`\n${result.errorText}\`\`\``;
56 | let prompt = '';
57 | const fileInfo = this.pickFileInfo(result.errorText);
58 |
59 | if (fileInfo?.path && fileInfo?.row && fileInfo?.col) {
60 | try {
61 | const codeSnippet = await this.resolveCodeSnippet(fileInfo.path, +fileInfo.row);
62 | if (codeSnippet) {
63 | prompt = `
64 | 在 IDE 中进行研发时,终端输出了一些与 typescript 有关的报错信息。
65 | 错误中的代码行内的代码为: \`${codeSnippet.lineCode}\`
66 | 代码行附近的 20 行代码为: \`\`\`\n${codeSnippet.snippet.join('\n')}\n\`\`\`
67 | 错误信息如下: ${result.errorText}
68 | 请给予上面的信息给出解决方案和代码建议
69 | `;
70 | }
71 | } catch {
72 | prompt = `在 IDE 中进行研发时,终端输出了一些报错信息,其中可能存在多个报错,需要你分别给出每个报错的解决方案,报错信息如下:\`\`\`\n${result.errorText}\n\`\`\``;
73 | }
74 | }
75 |
76 | return { message, prompt };
77 | }
78 |
79 | public pickFileInfo(errorText: string) {
80 | const fileReg = /(?[\w\/]+\.tsx?):(?\d+):(?\d+)/;
81 |
82 | const match = fileReg.exec(errorText);
83 |
84 | return match ? match.groups as { path: string; row: string; col: string } : undefined;
85 | }
86 |
87 | public async resolveCodeSnippet(filePath: string, row: number) {
88 | const workspaceFolderUri = this.workspaceService.getWorkspaceRootUri(undefined);
89 | if (!workspaceFolderUri) return
90 | const fileUri = workspaceFolderUri.resolve(filePath);
91 | const fileContent = await this.fileServiceClient.readFile(fileUri.toString());
92 | const fileContentLineArray = fileContent.content.toString().split('\n');
93 |
94 | return fileContentLineArray.length ? {
95 | snippet: fileContentLineArray.slice(Math.max(0, row - 10), row + 10),
96 | lineCode: fileContentLineArray[+row - 1]
97 | } : undefined;
98 | }
99 |
100 | public async generateShellPrompt(result: MatchResult) {
101 | const message = `${this.getMessagePrefix(result.operate)}:\`\`\`\n${result.errorText}\`\`\``;
102 | const inputPrompt = `请结合我的输入信息给出具体解决方案:输入信息:${result.input},`;
103 | const prompt = `在终端中输入命令遇到了报错,${result.input ? inputPrompt : '请给出可能的解决方案'},报错信息:\`\`\`\n${result.errorText}\n\`\`\` `;
104 |
105 | return { message, prompt };
106 | }
107 |
108 | public async generateJavaPrompt(result: MatchResult) {
109 | const message = `${this.getMessagePrefix(result.operate)}:\`\`\`\n${result.errorText}\`\`\``;
110 |
111 | const errorTextArray = result.errorText.split('\n');
112 | // 截取 10 行堆栈信息,过多会导致 token 超出上限
113 | const errorText = errorTextArray.slice(0, 10).join('\n');
114 | const prompt = `Java应用程序在运行过程中产生了一些报错,请根据报错信息,给出可能的解决方案,报错信息如下:\`\`\`\n${errorText}\n\`\`\` `;
115 |
116 | return { message, prompt };
117 | }
118 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ## [0.7.0](https://code.alipay.com/cloud-ide/codefuse-ide/compare/v0.6.4...v0.7.0) (2024-11-11)
6 |
7 |
8 | ### Features
9 |
10 | * 实现 Code Edits ([#56](https://code.alipay.com/cloud-ide/codefuse-ide/issues/56)) ([cca5fdb](https://code.alipay.com/cloud-ide/codefuse-ide/commit/cca5fdb7fc2432eae3343bb892f019e87b3701b9))
11 |
12 |
13 | ### Bug Fixes
14 |
15 | * **deps:** update opensumi packages to v3.4.5 ([#47](https://code.alipay.com/cloud-ide/codefuse-ide/issues/47)) ([0b8282f](https://code.alipay.com/cloud-ide/codefuse-ide/commit/0b8282f9ae976c6e772cada05f67b48e2c165c4e))
16 | * **deps:** update opensumi packages to v3.5.0 ([#49](https://code.alipay.com/cloud-ide/codefuse-ide/issues/49)) ([a5d9c5b](https://code.alipay.com/cloud-ide/codefuse-ide/commit/a5d9c5b244491c18bf057ea6788a181822cb102d))
17 |
18 | ### [0.6.4](https://code.alipay.com/cloud-ide/codefuse-ide/compare/v0.6.3...v0.6.4) (2024-10-16)
19 |
20 |
21 | ### Bug Fixes
22 |
23 | * set initialized after init ([b95d7b0](https://code.alipay.com/cloud-ide/codefuse-ide/commit/b95d7b04cee5185e6f8d0dcfab4dd481e0be106e))
24 |
25 | ### [0.6.3](https://code.alipay.com/cloud-ide/codefuse-ide/compare/v0.6.2...v0.6.3) (2024-10-16)
26 |
27 |
28 | ### Features
29 |
30 | * add minor ([551a6b2](https://code.alipay.com/cloud-ide/codefuse-ide/commit/551a6b2a42618ad530d6713475b5be26f1864b07))
31 |
32 |
33 | ### Bug Fixes
34 |
35 | * **deps:** update opensumi packages to v3.4.4 ([992b6d6](https://code.alipay.com/cloud-ide/codefuse-ide/commit/992b6d6431454eb036a22ead83d23b84925e4291))
36 |
37 | ### 0.6.2 (2024-10-11)
38 |
39 |
40 | ### Features
41 |
42 | * add open logo folder menu ([b5d275c](https://code.alipay.com/cloud-ide/codefuse-ide/commit/b5d275caca26568139436e13f0eba4d5b13dda56))
43 | * optimaze model config ([09df597](https://code.alipay.com/cloud-ide/codefuse-ide/commit/09df5970d18175431bb89c7af0154152d25956f5))
44 | * support ai lint and always show inline completions ([9cb41c0](https://code.alipay.com/cloud-ide/codefuse-ide/commit/9cb41c09e64afaa4eaa0cf032e8dcf3081586bca))
45 | * upgrade opensumi to 3.3.1-next-1725432779.0 ([c000fb2](https://code.alipay.com/cloud-ide/codefuse-ide/commit/c000fb2aae2acea0c79f1242e111f2627fdee573))
46 |
47 |
48 | ### Bug Fixes
49 |
50 | * **deps:** update opensumi packages to v3.4.0 ([fe2b072](https://code.alipay.com/cloud-ide/codefuse-ide/commit/fe2b0723de6ac5d6d01656f692f58848fde018c8))
51 | * **deps:** update opensumi packages to v3.4.1 ([80ad4eb](https://code.alipay.com/cloud-ide/codefuse-ide/commit/80ad4eb0bfaf8a60f786bcf3acd03573474ac1d0))
52 | * **deps:** update opensumi packages to v3.4.3 ([edd7e5c](https://code.alipay.com/cloud-ide/codefuse-ide/commit/edd7e5c1439cdfbcc1204bcf808ddd0419c0dd2c))
53 | * quit app after setTimeout ([f53684f](https://code.alipay.com/cloud-ide/codefuse-ide/commit/f53684fa401856aaba0381a4b4e0ba3306e24f35))
54 |
55 | ### 0.6.1 (2024-09-29)
56 |
57 |
58 | ### Features
59 |
60 | * add open logo folder menu ([b5d275c](https://code.alipay.com/cloud-ide/codefuse-ide/commit/b5d275caca26568139436e13f0eba4d5b13dda56))
61 | * optimaze model config ([09df597](https://code.alipay.com/cloud-ide/codefuse-ide/commit/09df5970d18175431bb89c7af0154152d25956f5))
62 | * support ai lint and always show inline completions ([9cb41c0](https://code.alipay.com/cloud-ide/codefuse-ide/commit/9cb41c09e64afaa4eaa0cf032e8dcf3081586bca))
63 | * upgrade opensumi to 3.3.1-next-1725432779.0 ([c000fb2](https://code.alipay.com/cloud-ide/codefuse-ide/commit/c000fb2aae2acea0c79f1242e111f2627fdee573))
64 |
65 |
66 | ### Bug Fixes
67 |
68 | * **deps:** update opensumi packages to v3.4.0 ([fe2b072](https://code.alipay.com/cloud-ide/codefuse-ide/commit/fe2b0723de6ac5d6d01656f692f58848fde018c8))
69 | * **deps:** update opensumi packages to v3.4.1 ([80ad4eb](https://code.alipay.com/cloud-ide/codefuse-ide/commit/80ad4eb0bfaf8a60f786bcf3acd03573474ac1d0))
70 | * quit app after setTimeout ([f53684f](https://code.alipay.com/cloud-ide/codefuse-ide/commit/f53684fa401856aaba0381a4b4e0ba3306e24f35))
71 |
72 | # [0.6.0](https://code.alipay.com/cloud-ide/codefuse-ide/compare/0.5.0...0.6.0) (2024-09-29)
73 |
74 |
75 | ### Bug Fixes
76 |
77 | * **deps:** update opensumi packages to v3.4.0 ([fe2b072](https://code.alipay.com/cloud-ide/codefuse-ide/commits/fe2b0723de6ac5d6d01656f692f58848fde018c8))
78 |
79 |
80 | ### Features
81 |
82 | * add open logo folder menu ([b5d275c](https://code.alipay.com/cloud-ide/codefuse-ide/commits/b5d275caca26568139436e13f0eba4d5b13dda56))
83 | * support ai lint and always show inline completions ([9cb41c0](https://code.alipay.com/cloud-ide/codefuse-ide/commits/9cb41c09e64afaa4eaa0cf032e8dcf3081586bca))
84 |
--------------------------------------------------------------------------------
/src/core/browser/header/header.view.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo, useRef, useState, useLayoutEffect } from 'react';
2 |
3 | import {
4 | ComponentRegistry,
5 | ComponentRenderer,
6 | Disposable,
7 | DomListener,
8 | IWindowService,
9 | electronEnv,
10 | getIcon,
11 | isMacintosh,
12 | useEventEffect,
13 | useInjectable,
14 | } from '@opensumi/ide-core-browser';
15 | import { LayoutViewSizeConfig } from '@opensumi/ide-core-browser/lib/layout/constants';
16 | import { IElectronMainUIService } from '@opensumi/ide-core-common/lib/electron';
17 | import { IElectronHeaderService } from '@opensumi/ide-electron-basic/lib/common/header';
18 |
19 | import styles from './header.module.less';
20 |
21 | const macTrafficWidth = 72;
22 | const winActionWidth = 138;
23 | const menuBarLeftWidth = 286
24 | const menuBarRightWidth = 28
25 | const extraWidth = 150
26 |
27 | const useMaximize = () => {
28 | const uiService: IElectronMainUIService = useInjectable(IElectronMainUIService);
29 |
30 | const [maximized, setMaximized] = useState(false);
31 |
32 | const getMaximized = async () => uiService.isMaximized(electronEnv.currentWindowId);
33 |
34 | useEffect(() => {
35 | const maximizeListener = uiService.on('maximizeStatusChange', (windowId, isMaximized) => {
36 | if (windowId === electronEnv.currentWindowId) {
37 | setMaximized(isMaximized);
38 | }
39 | });
40 | getMaximized().then((maximized) => {
41 | setMaximized(maximized);
42 | });
43 | return () => {
44 | maximizeListener.dispose();
45 | };
46 | }, []);
47 |
48 | return {
49 | maximized,
50 | getMaximized,
51 | };
52 | };
53 |
54 | /**
55 | * autoHide: Hide the HeaderBar when the macOS full screen
56 | */
57 | export const ElectronHeaderBar = () => {
58 | const ref = useRef(null)
59 | const windowService: IWindowService = useInjectable(IWindowService);
60 | const layoutViewSize = useInjectable(LayoutViewSizeConfig);
61 |
62 | const { getMaximized } = useMaximize();
63 |
64 | const safeHeight = useMemo(() => {
65 | return layoutViewSize.calcElectronHeaderHeight();
66 | }, [layoutViewSize]);
67 |
68 | useLayoutEffect(() => {
69 | const currentElement = ref.current
70 | if (!currentElement) return
71 | const { parentElement } = currentElement
72 | if (!parentElement) return
73 | if (isMacintosh) {
74 | parentElement.style.paddingLeft = `${macTrafficWidth}px`
75 | currentElement.style.left = `${macTrafficWidth + menuBarLeftWidth + extraWidth}px`
76 | currentElement.style.right = `${menuBarRightWidth + extraWidth}px`
77 | } else {
78 | parentElement.style.paddingRight = `${winActionWidth}px`
79 | currentElement.style.left = `${menuBarLeftWidth + extraWidth}px`
80 | currentElement.style.right = `${menuBarRightWidth + winActionWidth + extraWidth}px`
81 | }
82 | }, [])
83 |
84 | return (
85 | {
89 | if (await getMaximized()) {
90 | windowService.unmaximize();
91 | } else {
92 | windowService.maximize();
93 | }
94 | }}
95 | ref={ref}
96 | >
97 |
98 |
99 | );
100 | };
101 |
102 | export const HeaderBarTitleComponent = () => {
103 | const headerService = useInjectable(IElectronHeaderService) as IElectronHeaderService;
104 | const ref = useRef(null);
105 | const spanRef = useRef(null);
106 | const [appTitle, setAppTitle] = useState('');
107 |
108 | useEffect(() => {
109 | const defaultAppTitle = 'CodeFuse IDE'
110 | setAppTitle(headerService.appTitle || defaultAppTitle)
111 | const disposable = headerService.onTitleChanged((v) => {
112 | setAppTitle(v || defaultAppTitle);
113 | })
114 | return () => {
115 | disposable.dispose()
116 | }
117 | }, [])
118 |
119 | useEffect(() => {
120 | setPosition();
121 | const disposer = new Disposable();
122 |
123 | disposer.addDispose(
124 | new DomListener(window, 'resize', () => {
125 | setPosition();
126 | }),
127 | );
128 | }, []);
129 |
130 | function setPosition() {
131 | window.requestAnimationFrame(() => {
132 | if (ref.current && spanRef.current) {
133 | const windowWidth = window.innerWidth;
134 | const leftWidth = menuBarLeftWidth + extraWidth + (isMacintosh ? macTrafficWidth : 0)
135 | const left = Math.max(0, windowWidth * 0.5 - leftWidth - spanRef.current.offsetWidth * 0.5);
136 | ref.current.style.paddingLeft = left + 'px';
137 | ref.current.style.visibility = 'visible';
138 | }
139 | });
140 | }
141 |
142 | // 同时更新 document Title
143 | useEffect(() => {
144 | document.title = appTitle;
145 | }, [appTitle]);
146 |
147 | return (
148 |
149 | {appTitle}
150 |
151 | );
152 | };
153 |
--------------------------------------------------------------------------------
/src/bootstrap/watcher-host/index.ts:
--------------------------------------------------------------------------------
1 | import '@/core/common/asar';
2 | import { createConnection } from 'net';
3 |
4 | import { Injector } from '@opensumi/di';
5 | import { SumiConnectionMultiplexer } from '@opensumi/ide-connection';
6 | import { NetSocketConnection } from '@opensumi/ide-connection/lib/common/connection/drivers';
7 | import { argv } from '@opensumi/ide-core-common/lib/node/cli';
8 | import { suppressNodeJSEpipeError } from '@opensumi/ide-core-common/lib/node/utils';
9 | import { CommonProcessReporter, IReporter, ReporterProcessMessage } from '@opensumi/ide-core-common/lib/types';
10 | import { Emitter, isPromiseCanceledError } from '@opensumi/ide-utils';
11 |
12 | import { SUMI_WATCHER_PROCESS_SOCK_KEY, WATCHER_INIT_DATA_KEY } from '@opensumi/ide-file-service/lib/common';
13 |
14 | import { WatcherProcessLogger } from '@opensumi/ide-file-service/lib/node/hosted/watch-process-log';
15 | import { WatcherHostServiceImpl } from '@opensumi/ide-file-service/lib/node/hosted/watcher.host.service';
16 | import { LogServiceManager as LogServiceManagerToken } from '@opensumi/ide-logs/lib/node/log-manager';
17 | import { LogServiceManager } from '@/logger/node/log-manager';
18 |
19 | Error.stackTraceLimit = 100;
20 | const logger: any = console;
21 |
22 | async function initWatcherProcess() {
23 | patchConsole();
24 | patchProcess();
25 | const watcherInjector = new Injector();
26 | const reporterEmitter = new Emitter();
27 |
28 | watcherInjector.addProviders({
29 | token: IReporter,
30 | useValue: new CommonProcessReporter(reporterEmitter),
31 | }, {
32 | token: LogServiceManagerToken,
33 | useClass: LogServiceManager
34 | });
35 |
36 | const initData = JSON.parse(argv[WATCHER_INIT_DATA_KEY]);
37 | const connection = JSON.parse(argv[SUMI_WATCHER_PROCESS_SOCK_KEY]);
38 |
39 | const socket = createConnection(connection);
40 |
41 | const watcherProtocol = new SumiConnectionMultiplexer(new NetSocketConnection(socket), {
42 | timeout: -1,
43 | });
44 |
45 | const logger = new WatcherProcessLogger(watcherInjector, initData.logDir, initData.logLevel);
46 | const watcherHostService = new WatcherHostServiceImpl(watcherProtocol, logger);
47 | watcherHostService.initWatcherServer();
48 | }
49 |
50 | (async () => {
51 | await initWatcherProcess();
52 | })();
53 |
54 | function getErrorLogger() {
55 | // eslint-disable-next-line no-console
56 | return (logger && logger.error.bind(logger)) || console.error.bind(console);
57 | }
58 |
59 | function getWarnLogger() {
60 | // eslint-disable-next-line no-console
61 | return (logger && logger.warn.bind(logger)) || console.warn.bind(console);
62 | }
63 |
64 | function patchProcess() {
65 | process.exit = function (code?: number) {
66 | const err = new Error(`An extension called process.exit(${code ?? ''}) and this was prevented.`);
67 | getWarnLogger()(err.stack);
68 | } as (code?: number) => never;
69 |
70 | // override Electron's process.crash() method
71 | process.crash = function () {
72 | const err = new Error('An extension called process.crash() and this was prevented.');
73 | getWarnLogger()(err.stack);
74 | };
75 | }
76 |
77 | function _wrapConsoleMethod(method: 'log' | 'info' | 'warn' | 'error') {
78 | // eslint-disable-next-line no-console
79 | const original = console[method].bind(console);
80 |
81 | Object.defineProperty(console, method, {
82 | set: () => {
83 | // empty
84 | },
85 | get: () =>
86 | function (...args: any[]) {
87 | original(...args);
88 | },
89 | });
90 | }
91 |
92 | function patchConsole() {
93 | _wrapConsoleMethod('info');
94 | _wrapConsoleMethod('log');
95 | _wrapConsoleMethod('warn');
96 | _wrapConsoleMethod('error');
97 | }
98 |
99 | function unexpectedErrorHandler(e: any) {
100 | setTimeout(() => {
101 | getErrorLogger()('[Watcehr-Host]', e.message, e.stack && '\n\n' + e.stack);
102 | }, 0);
103 | }
104 |
105 | function onUnexpectedError(e: any) {
106 | let err = e;
107 | if (!err) {
108 | getWarnLogger()(`Unknown Exception ${err}`);
109 | return;
110 | }
111 |
112 | if (isPromiseCanceledError(err)) {
113 | getWarnLogger()(`Canceled ${err.message}`);
114 | return;
115 | }
116 |
117 | if (!(err instanceof Error)) {
118 | err = new Error(e);
119 | }
120 |
121 | unexpectedErrorHandler(err);
122 | }
123 |
124 | suppressNodeJSEpipeError(process, (msg) => {
125 | getErrorLogger()(msg);
126 | });
127 |
128 | process.on('uncaughtException', (err) => {
129 | onUnexpectedError(err);
130 | });
131 |
132 | const unhandledPromises: Promise[] = [];
133 | process.on('unhandledRejection', (reason, promise) => {
134 | unhandledPromises.push(promise);
135 | setTimeout(() => {
136 | const idx = unhandledPromises.indexOf(promise);
137 | if (idx >= 0) {
138 | promise.catch((e) => {
139 | unhandledPromises.splice(idx, 1);
140 | onUnexpectedError(e);
141 | });
142 | }
143 | }, 1000);
144 | });
145 |
146 | process.on('rejectionHandled', (promise: Promise) => {
147 | const idx = unhandledPromises.indexOf(promise);
148 | if (idx >= 0) {
149 | unhandledPromises.splice(idx, 1);
150 | }
151 | });
152 |
--------------------------------------------------------------------------------
/src/ai/browser/prompt.ts:
--------------------------------------------------------------------------------
1 | import { IMarkerErrorData } from '@opensumi/ide-ai-native/lib/browser/contrib/intelligent-completions/source/lint-error.source';
2 | import { EInlineOperation } from './constants'
3 |
4 | export const DefaultSystemPrompt = 'You are a powerful AI coding assistant working in CodeFuse IDE, a AI Native IDE based on CodeFuse and OpenSumi. You collaborate with a USER to solve coding tasks, which may involve creating, modifying, or debugging code, or answering questions. When the USER sends a message, relevant context (e.g., open files, cursor position, edit history, linter errors) may be attached. Use this information as needed.\n\n\nYou have access to tools to assist with tasks. Follow these rules:\n1. Always adhere to the tool call schema and provide all required parameters.\n2. Only use tools explicitly provided; ignore unavailable ones.\n3. Avoid mentioning tool names to the USER (e.g., say "I will edit your file" instead of "I need to use the edit_file tool").\n4. Only call tools when necessary; respond directly if the task is general or you already know the answer.\n5. Explain to the USER why you’re using a tool before calling it.\n\n\n\nWhen modifying code:\n1. Use code edit tools instead of outputting code unless explicitly requested.\n2. Limit tool calls to one per turn.\n3. Ensure generated code is immediately executable by including necessary imports, dependencies, and endpoints.\n4. For new projects, create a dependency management file (e.g., requirements.txt) and a README.\n5. For web apps, design a modern, user-friendly UI.\n6. Avoid generating non-textual or excessively long code.\n7. Read file contents before editing, unless appending a small change or creating a new file.\n8. Fix introduced linter errors if possible, but stop after 3 attempts and ask the USER for guidance.\n9. Reapply reasonable code edits if they weren’t followed initially.\n\n\nUse the appropriate tools to fulfill the USER’s request, ensuring all required parameters are provided or inferred from context.Always respond in 中文.';
5 |
6 | export const explainPrompt = (language: string, code: string) => {
7 | return `你将获得一段代码, 你的任务是以简洁的方式解释它,用中文回答。代码内容是: \n\`\`\`${language}\n${code}\n\`\`\``;
8 | };
9 |
10 | export const testPrompt = (code: string) => {
11 | return `为以下代码写单测:\n\`\`\`\n ${code}\n\`\`\``;
12 | };
13 |
14 | export const optimizePrompt = (code: string) => {
15 | return `优化以下代码:\n\`\`\`\n ${code}\`\`\``;
16 | };
17 |
18 | export const commentsPrompt = (code: string) => {
19 | return `帮我将下面这段代码加入中文注释,原来的代码的代码请按照原样返回,不要添加任何额外字符包括空格:\n\`\`\`\n${code}\`\`\``;
20 | };
21 |
22 | export const detectIntentPrompt = (input: string) => {
23 | return `
24 | 在我的编辑器中,存在一些指令,这些指令可以被分成几组,下面给出全部的分组及分组简介,请针对用户给出的提问,找到对应的分组,并直接返回分组名称
25 |
26 | 指令分组:
27 | * [${EInlineOperation.Explain}]: 解释代码,代码解释,用于对代码的解释,能够用自然语言解释代码的意思,它能够理解并分析各种编程语言的代码,并提供清晰、准确、易于理解的解释。
28 | * [${EInlineOperation.Comments}]: 添加注释,用于给代码添加注释
29 | * [${EInlineOperation.Test}]: 生成单测,用于生成单元测试用例,能够对代码进行单元测试的生成,生成测试代码,生成代码的测试
30 | * [${EInlineOperation.Optimize}]: 优化代码,用于对代码进行优化,能够优化代码,使其代码更加合理
31 | * [None]: 表示用户的提问并不适合以上任意一个分组,则返回 None
32 |
33 | 提问: ${input}
34 | 回答: [分组名称],请返回上述的指令分组名称,不要包含其它内容
35 | `;
36 | };
37 |
38 | export const terminalCommandSuggestionPrompt = (message: string) => {
39 | return `
40 | 你是一个 Shell 脚本专家,现在我需要使用 Shell 来完成一些操作,但是我不熟悉 Shell 命令,因此我需要通过自然语言描述生成终端命令,只需生成 1 到 5 个命令。
41 | 提示:使用 . 来表示当前文件夹
42 | 下面是自然语言描述和其对应的终端命令:
43 | 提问: 查看机器内存
44 | 回答:
45 | #Command#: free -m
46 | #Description#: 查看机器内存
47 | 提问: 查看当前进程的 pid
48 | 回答:
49 | #Command#: echo$$
50 | #Description#: 查看当前进程的 pid
51 | 提问: ${message}`;
52 | };
53 |
54 | export class RenamePromptManager {
55 | static requestPrompt(language: string, varName: string, above: string, below: string) {
56 | const prompt = `
57 | 我需要你的帮助,请帮我推荐 5 个指定变量的重命名候选项。
58 | 我希望这些新的变量名能更符合代码上下文、整段代码的风格,更有意义。
59 |
60 | 我会将代码分成三段发给你,每段代码用 --- 进行包裹。这些代码是一段 ${language} 代码片段。
61 | 第一段代码是该变量之前的上文,第二段是变量名,第三段是该变量的下文。
62 |
63 | ---
64 | ${above.slice(-500)}
65 | ---
66 |
67 | ---
68 | ${varName}
69 | ---
70 |
71 | ---
72 | ${below.slice(0, 500)}
73 | ---
74 |
75 |
76 | 你的任务是:
77 | 请根据上下文以及代码的作用帮我推荐一下 ${varName} 能替换成哪些变量名,仅需要把所有可能的变量名输出,不用输出所有的代码。将结果放在代码块中(用 \`\`\` 包裹),每行一个,不用带序号。`;
78 | return prompt;
79 | }
80 |
81 | static extractResponse(data: string) {
82 | const codeBlock = /```([\s\S]*?)```/g;
83 | const result = data.match(codeBlock);
84 |
85 | if (!result) {
86 | return [];
87 | }
88 |
89 | const lines = result[0].replace(/```/g, '').trim().split('\n');
90 | return lines;
91 | }
92 | }
93 |
94 |
95 | export const codeEditsLintErrorPrompt = (text: string, errors: IMarkerErrorData[]) => {
96 | return `
97 | #Role: 代码领域的 IDE 专家
98 |
99 | #Profile:
100 | - description: 熟悉各种编程语言并擅长解决由语言服务引起的各种问题,能够快速定位问题并提供解决方案,专注于代码质量和错误修复的专家
101 |
102 | ##Goals:
103 | - 修复代码中的 error 错误,提升代码质量
104 |
105 | ##Constrains:
106 | - 仅修改必要的代码以修复错误
107 | - 保持代码的原始功能和逻辑不变
108 | - 保持代码的缩进规则不变,这是强规定,你需要检查代码的缩进规则,并保持这个缩进规则
109 |
110 | ##Skills:
111 | - 熟悉 Java/TypeScript/JavaScript/Python 等语言
112 | - 能够根据错误信息快速定位问题并提供解决方案
113 |
114 | ##Workflows:
115 | - 分析提供的代码和错误信息
116 | - 提供修复步骤和修改后的代码
117 |
118 | ##CodeSnippet:
119 | - 以下是有问题的代码片段
120 | \`\`\`
121 | ${text}
122 | \`\`\`
123 |
124 | ##LintErrors:
125 | ${JSON.stringify(errors.map(e => ({ message: e.message })))}
126 |
127 | 请根据上述错误信息,直接提供修复后的代码,不需要解释
128 | `;
129 | };
130 |
--------------------------------------------------------------------------------
/src/logger/common/log-service.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BaseLogServiceOptions,
3 | LogLevel,
4 | format,
5 | } from '@opensumi/ide-logs';
6 | import { uuid } from '@opensumi/ide-core-common';
7 | import type * as spdlog from '@vscode/spdlog'
8 | import { ILogService } from './types'
9 |
10 | interface ILog {
11 | level: LogLevel;
12 | message: string;
13 | }
14 |
15 | interface ILogServiceOptions {
16 | logPath: string;
17 | logLevel: LogLevel;
18 | pid?: number;
19 | }
20 |
21 | enum SpdLogLevel {
22 | Trace,
23 | Debug,
24 | Info,
25 | Warning,
26 | Error,
27 | Critical,
28 | Off
29 | }
30 |
31 | export abstract class AbstractLogService implements ILogService {
32 | protected logger: SpdLogger | undefined;
33 | protected logPath: string;
34 | protected logLevel: LogLevel;
35 |
36 | constructor(options: ILogServiceOptions) {
37 | this.logPath = options.logPath;
38 | this.logLevel = options.logLevel || LogLevel.Info;
39 | }
40 |
41 | abstract sendLog(level: LogLevel, message: string): void
42 |
43 | protected shouldLog(level: LogLevel): boolean {
44 | return this.getLevel() <= level;
45 | }
46 |
47 | verbose(): void {
48 | if (!this.shouldLog(LogLevel.Verbose)) return
49 | this.sendLog(LogLevel.Verbose, format(arguments));
50 | }
51 |
52 | debug(): void {
53 | if (!this.shouldLog(LogLevel.Debug)) return
54 | this.sendLog(LogLevel.Debug, format(arguments));
55 | }
56 |
57 | log(): void {
58 | if (!this.shouldLog(LogLevel.Info)) return
59 | this.sendLog(LogLevel.Info, format(arguments));
60 | }
61 |
62 | info(): void {
63 | if (!this.shouldLog(LogLevel.Info)) return
64 | this.sendLog(LogLevel.Info, format(arguments));
65 | }
66 |
67 | warn(): void {
68 | if (!this.shouldLog(LogLevel.Warning)) return
69 | this.sendLog(LogLevel.Warning, format(arguments));
70 | }
71 |
72 | error(): void {
73 | if (!this.shouldLog(LogLevel.Error)) return
74 | const arg = arguments[0];
75 | let message: string;
76 |
77 | if (arg instanceof Error) {
78 | const array = Array.prototype.slice.call(arguments) as any[];
79 | array[0] = arg.stack;
80 | message = format(array);
81 | this.sendLog(LogLevel.Error, message);
82 | } else {
83 | message = format(arguments);
84 | this.sendLog(LogLevel.Error, message);
85 | }
86 | }
87 |
88 | critical(): void {
89 | if (!this.shouldLog(LogLevel.Critical)) return
90 | this.sendLog(LogLevel.Critical, format(arguments));
91 | }
92 |
93 | setOptions(options: BaseLogServiceOptions) {
94 | if (options.logLevel) {
95 | this.logLevel = options.logLevel;
96 | }
97 | }
98 |
99 | getLevel(): LogLevel {
100 | return this.logLevel;
101 | }
102 |
103 | setLevel(level: LogLevel): void {
104 | this.logLevel = level;
105 | }
106 |
107 | async drop() {}
108 |
109 | async flush() {}
110 |
111 | dispose() {}
112 | }
113 |
114 | export class SpdLogger extends AbstractLogService {
115 | #buffer: ILog[] = [];
116 | #spdLoggerCreatePromise: Promise;
117 | #logger: spdlog.Logger | undefined;
118 |
119 | constructor(options: ILogServiceOptions) {
120 | super(options);
121 | this.#spdLoggerCreatePromise = this.#createSpdLogLogger();
122 | }
123 |
124 | async #createSpdLogLogger(): Promise {
125 | const fileCount = 6;
126 | const fileSize = 5 * 1024 * 1024;
127 | try {
128 | const _spdlog = await import('@vscode/spdlog');
129 | _spdlog.setFlushOn(SpdLogLevel.Trace);
130 | const logger = await _spdlog.createAsyncRotatingLogger(uuid(), this.logPath, fileSize, fileCount);
131 | this.#logger = logger;
132 | logger.setPattern('%Y-%m-%d %H:%M:%S.%e [%l] %v');
133 | logger.setLevel(this.getSpdLogLevel(this.getLevel()))
134 | for (const { level, message } of this.#buffer) {
135 | this.sendLog( level, message);
136 | }
137 | this.#buffer = [];
138 | } catch (e) {
139 | console.error(e);
140 | }
141 | }
142 |
143 | sendLog(level: LogLevel, message: string): void {
144 | if (this.#logger) {
145 | switch (level) {
146 | case LogLevel.Verbose:
147 | return this.#logger.trace(message);
148 | case LogLevel.Debug:
149 | return this.#logger.debug(message);
150 | case LogLevel.Info:
151 | return this.#logger.info(message);
152 | case LogLevel.Warning:
153 | return this.#logger.warn(message);
154 | case LogLevel.Error:
155 | return this.#logger.error(message);
156 | case LogLevel.Critical:
157 | return this.#logger.critical(message);
158 | default:
159 | throw new Error('Invalid log level');
160 | }
161 | } else if (this.getLevel() <= level) {
162 | this.#buffer.push({ level, message });
163 | }
164 | }
165 |
166 | override async flush() {
167 | if (this.#logger) {
168 | this.#logger.flush();
169 | } else {
170 | this.#spdLoggerCreatePromise.then(() => this.flush());
171 | }
172 | }
173 |
174 | override async drop() {
175 | if (this.#logger) {
176 | this.#logger.drop();
177 | } else {
178 | return this.#spdLoggerCreatePromise.then(() => this.drop());
179 | }
180 | }
181 |
182 | override dispose(): void {
183 | this.drop();
184 | }
185 |
186 | private getSpdLogLevel(level: LogLevel): SpdLogLevel {
187 | switch (level) {
188 | case LogLevel.Verbose: return SpdLogLevel.Trace;
189 | case LogLevel.Debug: return SpdLogLevel.Debug;
190 | case LogLevel.Info: return SpdLogLevel.Info;
191 | case LogLevel.Warning: return SpdLogLevel.Warning;
192 | case LogLevel.Error: return SpdLogLevel.Error;
193 | case LogLevel.Critical: return SpdLogLevel.Critical;
194 | default: return SpdLogLevel.Off;
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/src/core/browser/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/ai/browser/ai-run.service.ts:
--------------------------------------------------------------------------------
1 | import * as jsoncparser from 'jsonc-parser';
2 | import { Injectable, Autowired } from '@opensumi/di';
3 | import { PreferenceConfigurations } from '@opensumi/ide-core-browser';
4 | import { CommandService, URI, FileType, ChatServiceToken } from '@opensumi/ide-core-common';
5 | import { DEBUG_COMMANDS, DebugConfiguration } from '@opensumi/ide-debug';
6 | import { DebugConfigurationManager } from '@opensumi/ide-debug/lib/browser/debug-configuration-manager';
7 | import { WorkbenchEditorService } from '@opensumi/ide-editor';
8 | import { IFileServiceClient } from '@opensumi/ide-file-service';
9 | import { IWorkspaceService } from '@opensumi/ide-workspace';
10 | import { ChatService } from '@opensumi/ide-ai-native/lib/browser/chat/chat.api.service';
11 | import { AIBackSerivcePath } from '@opensumi/ide-core-common';
12 | import type { IAIBackService } from '@opensumi/ide-core-common';
13 | import { MessageService } from '@opensumi/ide-overlay/lib/browser/message.service';
14 | import { IAIReporter } from '@opensumi/ide-core-common';
15 |
16 | // 暂定的技术栈集合
17 | export enum EStackName {
18 | NODEJS = 'node.js',
19 | JAVA = 'java',
20 | MINI_PROGRAM = 'mini program',
21 | PYTHON = 'python',
22 | C_CPP = 'c/c++',
23 | GO = 'go',
24 | rust = 'rust',
25 | FRONT_END = 'front end',
26 | EXTENSION = 'ide extension',
27 | EMPTY = 'empty',
28 | }
29 |
30 | const EStackNameKeys = Object.keys(EStackName) as (keyof typeof EStackName)[];
31 |
32 | /**
33 | * 智能运行的逻辑规则如下
34 | * 以 launch.json 配置为主
35 | * a. 如果没有该文件,则智能生成该文件(走 AI)
36 | * b. 如果有该文件,则默认运行第一条(后续可以配置)
37 | */
38 | @Injectable()
39 | export class AiRunService {
40 | @Autowired(AIBackSerivcePath)
41 | aiBackService: IAIBackService;
42 |
43 | @Autowired(CommandService)
44 | protected readonly commandService: CommandService;
45 |
46 | @Autowired(MessageService)
47 | protected readonly messageService: MessageService;
48 |
49 | @Autowired(IWorkspaceService)
50 | protected readonly workspaceService: IWorkspaceService;
51 |
52 | @Autowired(PreferenceConfigurations)
53 | protected readonly preferenceConfigurations: PreferenceConfigurations;
54 |
55 | @Autowired(DebugConfigurationManager)
56 | protected readonly debugConfigurationManager: DebugConfigurationManager;
57 |
58 | @Autowired(WorkbenchEditorService)
59 | protected readonly workbenchEditorService: WorkbenchEditorService;
60 |
61 | @Autowired(IFileServiceClient)
62 | private readonly fileSystem: IFileServiceClient;
63 |
64 | @Autowired(ChatServiceToken)
65 | protected readonly aiChatService: ChatService;
66 |
67 | @Autowired(IAIReporter)
68 | aiReporter: IAIReporter;
69 |
70 | get pkgUri() {
71 | const workspaceFolderUri = this.workspaceService.getWorkspaceRootUri(undefined);
72 | if (!workspaceFolderUri) {
73 | return null;
74 | }
75 | return workspaceFolderUri.resolve('package.json')
76 | }
77 |
78 | private async readResourceContent(resource: URI): Promise {
79 | try {
80 | const { content } = await this.fileSystem.readFile(resource.toString());
81 | return content.toString();
82 | } catch (error) {
83 | return '';
84 | }
85 | }
86 |
87 | public async containPackageJson(): Promise {
88 | const { pkgUri } = this;
89 | if (!pkgUri) return false;
90 | const stat = await this.fileSystem.getFileStat(pkgUri.toString());
91 | if (!stat) return false
92 | return !stat.isDirectory
93 | }
94 |
95 | public async getNodejsDebugConfigurations() {
96 | if (!(await this.containPackageJson())) {
97 | this.messageService.info(
98 | '项目无 package.json,无法生成运行配置'
99 | );
100 | return
101 | }
102 |
103 | const fileContent = await this.readResourceContent(this.pkgUri!);
104 |
105 | const parseJson = jsoncparser.parse(fileContent);
106 |
107 | const jsonContent = JSON.stringify(
108 | {
109 | name: parseJson.name || '',
110 | version: parseJson.version || '',
111 | description: parseJson.description || '',
112 | egg: parseJson.egg || '',
113 | bin: parseJson.bin || '',
114 | scripts: parseJson.scripts
115 | },
116 | undefined,
117 | 1
118 | );
119 |
120 | const prompt = `我会给你一个项目类型和 package.json 文件内容。你需要通过分析里面的 scripts 内容,找到合适的运行命令来启动项目。如果找到合适的命令后直接返回,不需要解释。请参照下面的示例问答的格式返回。
121 | 提问: 这是一个 node.js 项目,package.json 的文件内容是 \`\`\`\n${JSON.stringify({
122 | scripts: { dev: 'npm run dev', test: 'npm run test' }
123 | })}\n\`\`\`
124 | 回答: "dev"
125 | 提问: 这是一个 front-end 项目,package.json 的文件内容是 \`\`\`\n${JSON.stringify({
126 | scripts: { start: 'npm run start', build: 'npm run build' }
127 | })}\n\`\`\`
128 | 回答: "start"
129 | 提问: 这是一个 Node.js 项目,package.json 的文件内容是 \`\`\`\n${jsonContent}\n\`\`\`
130 | `;
131 |
132 | const reportRelationId = this.aiReporter.start('aiRunFrontEnd', { message: 'aiRunFrontEnd' });
133 |
134 | const res = await this.aiBackService.request(prompt, {
135 | // @ts-ignore
136 | maxTokens: 1600,
137 | enableGptCache: false
138 | });
139 |
140 | if (!res.data || res.errorCode !== 0) {
141 | res.errorMsg && this.messageService.info(res.errorMsg);
142 | // todo: 类型不对
143 | this.aiReporter.end(reportRelationId, { message: 'aiRunFrontEndModelFetchError', success: false });
144 | return undefined;
145 | }
146 |
147 | const regex = /(?:(?:npm|cnpm) run|yarn) (\w+)/; // 解析命令的正则表达式
148 | const backtickRegex = /`(?:(?:npm|cnpm) run|yarn) (\w+)`/; // 反引号包围的命令匹配
149 |
150 | // 尝试匹配基础命令
151 | let match = res.data.match(regex);
152 | // 如果基础命令未匹配,尝试匹配反引号包围的命令
153 | if (!match) {
154 | match = res.data.match(backtickRegex);
155 | }
156 |
157 | let command = '';
158 | if (match) {
159 | command = match[1]; // 提取匹配的命令名
160 | } else {
161 | // todo: 类型不对
162 | // this.aiReporter.end(reportRelationId, { message: 'aiRunFrontEndModelResponseUNExpected', runSuccess: false });
163 | return undefined;
164 | }
165 |
166 | // todo: 类型不对
167 | // this.aiReporter.end(reportRelationId, { message: 'aiRunFrontEndSuccess', runSuccess: true });
168 |
169 | // const [, command] = value;
170 | const configuration: DebugConfiguration = {
171 | name: `Run npm ${command}`,
172 | type: 'node',
173 | request: 'launch',
174 | runtimeExecutable: 'npm',
175 | runtimeArgs: ['run', `${command}`],
176 | cwd: '${workspaceFolder}',
177 | console: 'integratedTerminal',
178 | autoPick: true // 跳过 quickPick 自动选择(此选项不会落到用户配置中)
179 | };
180 |
181 | return [configuration];
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/src/ai/node/ai-back.service.ts:
--------------------------------------------------------------------------------
1 | import { pipeline } from 'node:stream';
2 | import { Autowired, Injectable } from '@opensumi/di';
3 | import { ChatCompletionRequestMessage, ChatCompletionRequestMessageRoleEnum } from '@opensumi/ide-ai-native/lib/common';
4 | import { IAIBackService, IAICompletionOption, IAIReportCompletionOption, IAIBackServiceOption } from '@opensumi/ide-core-common';
5 | import { IAIBackServiceResponse, IChatContent } from '@opensumi/ide-core-common/lib/types/ai-native';
6 | import { CancellationToken, INodeLogger } from '@opensumi/ide-core-node';
7 | import { BaseAIBackService, ChatReadableStream } from '@opensumi/ide-core-node/lib/ai-native/base-back.service';
8 | import type { Response, fetch as FetchType } from 'undici-types';
9 | import { ILogServiceManager } from '@opensumi/ide-logs';
10 | import { AnthropicModel } from '@opensumi/ide-ai-native/lib/node/anthropic/anthropic-language-model';
11 | import { DeepSeekModel } from '@opensumi/ide-ai-native/lib/node/deepseek/deepseek-language-model';
12 | import { OpenAIModel } from '@opensumi/ide-ai-native/lib/node/openai/openai-language-model';
13 | import { OpenAICompatibleModel } from '@opensumi/ide-ai-native/lib/node/openai-compatible/openai-compatible-language-model';
14 |
15 | import { ChatCompletion, Completion } from './types';
16 | import { AIModelService } from './model.service'
17 |
18 | @Injectable()
19 | export class AIBackService extends BaseAIBackService implements IAIBackService {
20 | private logger: INodeLogger
21 |
22 | @Autowired(ILogServiceManager)
23 | private readonly loggerManager: ILogServiceManager;
24 |
25 | @Autowired(AIModelService)
26 | modelService: AIModelService
27 |
28 | @Autowired(AnthropicModel)
29 | protected readonly anthropicModel: AnthropicModel;
30 |
31 | @Autowired(OpenAIModel)
32 | protected readonly openaiModel: OpenAIModel;
33 |
34 | @Autowired(DeepSeekModel)
35 | protected readonly deepseekModel: DeepSeekModel;
36 |
37 | @Autowired(OpenAICompatibleModel)
38 | protected readonly openAICompatibleModel: OpenAICompatibleModel;
39 |
40 | constructor() {
41 | super();
42 | this.logger = this.loggerManager.getLogger('ai' as any);
43 | }
44 |
45 | override async requestStream(input: string, options: IAIBackServiceOption, cancelToken?: CancellationToken) {
46 | const chatReadableStream = new ChatReadableStream();
47 | cancelToken?.onCancellationRequested(() => {
48 | chatReadableStream.abort();
49 | });
50 |
51 | const model = options.model;
52 |
53 | if (model === 'openai') {
54 | this.openaiModel.request(input, chatReadableStream, options, cancelToken);
55 | } else if (model === 'deepseek') {
56 | this.deepseekModel.request(input, chatReadableStream, options, cancelToken);
57 | } else if (model === 'anthropic') {
58 | this.anthropicModel.request(input, chatReadableStream, options, cancelToken);
59 | } else {
60 | this.openAICompatibleModel.request(input, chatReadableStream, options, cancelToken);
61 | }
62 |
63 | return chatReadableStream;
64 | }
65 |
66 | async requestCompletion(input: IAICompletionOption, cancelToken?: CancellationToken) {
67 | const config = this.getCompletionConfig()
68 | if (!config) {
69 | return {
70 | sessionId: input.sessionId,
71 | codeModelList: [],
72 | }
73 | }
74 |
75 | const response = await this.fetchModel(
76 | this.getCompletionUrl(config.baseUrl, !config.codeFimTemplate),
77 | {
78 | stream: false,
79 | model: config.codeModelName,
80 | max_tokens: config.codeMaxTokens,
81 | temperature: config.codeTemperature,
82 | presence_penalty: config.codePresencePenalty,
83 | frequency_penalty: config.codeFrequencyPenalty,
84 | top_p: config.codeTopP,
85 | ...(config.codeFimTemplate ? {
86 | messages: [
87 | ...(config.codeSystemPrompt ? [
88 | {
89 | role: ChatCompletionRequestMessageRoleEnum.System,
90 | content: config.codeSystemPrompt,
91 | },
92 | ] : []),
93 | {
94 | role: ChatCompletionRequestMessageRoleEnum.User,
95 | content: config.codeFimTemplate.replace('{prefix}', input.prompt).replace('{suffix}', input.suffix || ''),
96 | }
97 | ]
98 | } : {
99 | prompt: input.prompt,
100 | suffix: input.suffix,
101 | })
102 | },
103 | cancelToken
104 | );
105 |
106 | if (!response.ok) {
107 | this.logger.error(`ai request completion failed: status: ${response.status}, body: ${await response.text()}`);
108 | return {
109 | sessionId: input.sessionId,
110 | codeModelList: [],
111 | }
112 | }
113 |
114 | try {
115 | const data = await response.json() as ChatCompletion | Completion
116 | const content = config.codeFimTemplate ? (data as ChatCompletion)?.choices?.[0]?.message?.content : (data as Completion)?.choices?.[0]?.text;
117 | if (!content) {
118 | return {
119 | sessionId: input.sessionId,
120 | codeModelList: [],
121 | }
122 | }
123 | return {
124 | sessionId: input.sessionId,
125 | codeModelList: [{ content }],
126 | }
127 | } catch (err: any) {
128 | this.logger.error(`ai request completion body parse error: ${err?.message}`);
129 | throw err
130 | }
131 | }
132 |
133 | private getCompletionConfig() {
134 | const { config } = this.modelService
135 | if (!config) {
136 | this.logger.warn('miss config')
137 | return null
138 | }
139 | if (!config.baseUrl) {
140 | this.logger.warn('miss config baseUrl')
141 | return null
142 | }
143 | const modelName = config.codeModelName
144 | if (!modelName) {
145 | this.logger.warn('miss config modelName')
146 | return null
147 | }
148 | return config;
149 | }
150 |
151 | private async fetchModel(url: string | URL, body: Record, cancelToken?: CancellationToken): Promise {
152 | const controller = new AbortController();
153 | const signal = controller.signal;
154 |
155 | const { config } = this.modelService
156 |
157 | cancelToken?.onCancellationRequested(() => {
158 | controller.abort();
159 | });
160 |
161 | return fetch(
162 | url,
163 | {
164 | signal,
165 | method: 'POST',
166 | headers: {
167 | 'Content-Type': 'application/json;charset=UTF-8',
168 | ...(config?.apiKey ? {
169 | Authorization: `Bearer ${config.apiKey}`
170 | } : null),
171 | },
172 | body: JSON.stringify(body),
173 | },
174 | ) as unknown as Promise;
175 | }
176 |
177 | private getCompletionUrl(baseUrl: string, supportFim = false) {
178 | if (!baseUrl.endsWith('/')) {
179 | baseUrl += '/'
180 | }
181 | return new URL(supportFim ? 'completions' : 'chat/completions', baseUrl);
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "codefuse-ide",
3 | "version": "0.7.0",
4 | "description": "CodeFuse AI Native IDE",
5 | "main": "out/main",
6 | "scripts": {
7 | "start": "electron-forge start",
8 | "start-web": "ts-node build/webpack-web/web-start.ts",
9 | "electron-rebuild": "node -r ts-node/register ./build/rebuild.ts",
10 | "web-rebuild": "node -r ts-node/register ./build/rebuild.ts --target=web",
11 | "build-web": "webpack --config ./build/webpack-web/webpack.config.ts --progress --color",
12 | "package": "electron-forge package",
13 | "make": "electron-forge make",
14 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
15 | "release": "standard-version",
16 | "release:minor": "npm run release -- --release-as minor"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git@code.alipay.com:cloud-ide/codefuse-ide.git"
21 | },
22 | "keywords": [
23 | "AI",
24 | "IDE"
25 | ],
26 | "author": {
27 | "name": "CodeFuse"
28 | },
29 | "license": "MIT",
30 | "devDependencies": {
31 | "@electron-forge/cli": "^7.5.0",
32 | "@electron-forge/maker-squirrel": "^7.5.0",
33 | "@electron-forge/maker-zip": "^7.4.0",
34 | "@electron-forge/plugin-base": "^7.4.0",
35 | "@electron-forge/plugin-webpack": "^7.4.0",
36 | "@electron-forge/shared-types": "^7.4.0",
37 | "@electron-forge/web-multi-logger": "^7.5.0",
38 | "@types/debug": "^4",
39 | "@types/js-yaml": "^4",
40 | "@types/koa": "^2",
41 | "@types/node": "^22.8.1",
42 | "@types/react": "^18.3.3",
43 | "@types/react-dom": "^18.3.1",
44 | "app-builder-bin": "^4.2.0",
45 | "app-builder-lib": "25.1.8",
46 | "asar": "^3.2.0",
47 | "chalk": "^4.0.0",
48 | "conventional-changelog-cli": "^5.0.0",
49 | "copy-webpack-plugin": "^12.0.2",
50 | "cross-env": "^7.0.3",
51 | "css-loader": "^6",
52 | "debug": "^4.3.7",
53 | "electron": "30.1.2",
54 | "extract-zip": "^2.0.1",
55 | "fast-glob": "^3.3.2",
56 | "glob": "^11.0.0",
57 | "html-webpack-plugin": "^5.6.2",
58 | "less": "^4.2.0",
59 | "less-loader": "^12.2.0",
60 | "listr2": "^8.2.5",
61 | "mini-css-extract-plugin": "^2.9.0",
62 | "node-gyp": "^10.2.0",
63 | "node-polyfill-webpack-plugin": "^4.0.0",
64 | "npm-run-all": "^4.1.5",
65 | "null-loader": "^4.0.1",
66 | "optimize-css-assets-webpack-plugin": "^6.0.1",
67 | "standard-version": "^9.5.0",
68 | "style-loader": "^4.0.0",
69 | "style-resources-loader": "^1.5.0",
70 | "ts-loader": "^9.5.1",
71 | "ts-node": "^10.9.2",
72 | "tsconfig-paths": "^4.2.0",
73 | "tsconfig-paths-webpack-plugin": "^4.1.0",
74 | "typescript": "^5.5.3",
75 | "undici-types": "^6.20.0",
76 | "webpack": "^5.94.0",
77 | "webpack-cli": "^5.1.4",
78 | "webpack-dev-server": "^5.2.0",
79 | "webpack-merge": "^6.0.1",
80 | "webpack-node-externals": "^3.0.0",
81 | "yauzl": "^3.1.3"
82 | },
83 | "dependencies": {
84 | "@opensumi/ide-addons": "3.8.1-next-1741253659.0",
85 | "@opensumi/ide-ai-native": "3.8.1-next-1741253659.0",
86 | "@opensumi/ide-comments": "3.8.1-next-1741253659.0",
87 | "@opensumi/ide-core-browser": "3.8.1-next-1741253659.0",
88 | "@opensumi/ide-core-common": "3.8.1-next-1741253659.0",
89 | "@opensumi/ide-core-electron-main": "3.8.1-next-1741253659.0",
90 | "@opensumi/ide-core-node": "3.8.1-next-1741253659.0",
91 | "@opensumi/ide-debug": "3.8.1-next-1741253659.0",
92 | "@opensumi/ide-decoration": "3.8.1-next-1741253659.0",
93 | "@opensumi/ide-design": "3.8.1-next-1741253659.0",
94 | "@opensumi/ide-editor": "3.8.1-next-1741253659.0",
95 | "@opensumi/ide-electron-basic": "3.8.1-next-1741253659.0",
96 | "@opensumi/ide-explorer": "3.8.1-next-1741253659.0",
97 | "@opensumi/ide-express-file-server": "3.8.1-next-1741253659.0",
98 | "@opensumi/ide-extension": "3.8.1-next-1741253659.0",
99 | "@opensumi/ide-extension-manager": "3.8.1-next-1741253659.0",
100 | "@opensumi/ide-extension-storage": "3.8.1-next-1741253659.0",
101 | "@opensumi/ide-file-scheme": "3.8.1-next-1741253659.0",
102 | "@opensumi/ide-file-search": "3.8.1-next-1741253659.0",
103 | "@opensumi/ide-file-service": "3.8.1-next-1741253659.0",
104 | "@opensumi/ide-file-tree-next": "3.8.1-next-1741253659.0",
105 | "@opensumi/ide-i18n": "3.8.1-next-1741253659.0",
106 | "@opensumi/ide-keymaps": "3.8.1-next-1741253659.0",
107 | "@opensumi/ide-logs": "3.8.1-next-1741253659.0",
108 | "@opensumi/ide-main-layout": "3.8.1-next-1741253659.0",
109 | "@opensumi/ide-markdown": "3.8.1-next-1741253659.0",
110 | "@opensumi/ide-markers": "3.8.1-next-1741253659.0",
111 | "@opensumi/ide-menu-bar": "3.8.1-next-1741253659.0",
112 | "@opensumi/ide-monaco": "3.8.1-next-1741253659.0",
113 | "@opensumi/ide-monaco-enhance": "3.8.1-next-1741253659.0",
114 | "@opensumi/ide-opened-editor": "3.8.1-next-1741253659.0",
115 | "@opensumi/ide-outline": "3.8.1-next-1741253659.0",
116 | "@opensumi/ide-output": "3.8.1-next-1741253659.0",
117 | "@opensumi/ide-overlay": "3.8.1-next-1741253659.0",
118 | "@opensumi/ide-preferences": "3.8.1-next-1741253659.0",
119 | "@opensumi/ide-process": "3.8.1-next-1741253659.0",
120 | "@opensumi/ide-quick-open": "3.8.1-next-1741253659.0",
121 | "@opensumi/ide-remote-opener": "3.8.1-next-1741253659.0",
122 | "@opensumi/ide-scm": "3.8.1-next-1741253659.0",
123 | "@opensumi/ide-search": "3.8.1-next-1741253659.0",
124 | "@opensumi/ide-status-bar": "3.8.1-next-1741253659.0",
125 | "@opensumi/ide-storage": "3.8.1-next-1741253659.0",
126 | "@opensumi/ide-task": "3.8.1-next-1741253659.0",
127 | "@opensumi/ide-terminal-next": "3.8.1-next-1741253659.0",
128 | "@opensumi/ide-testing": "3.8.1-next-1741253659.0",
129 | "@opensumi/ide-theme": "3.8.1-next-1741253659.0",
130 | "@opensumi/ide-toolbar": "3.8.1-next-1741253659.0",
131 | "@opensumi/ide-variable": "3.8.1-next-1741253659.0",
132 | "@opensumi/ide-webview": "3.8.1-next-1741253659.0",
133 | "@opensumi/ide-workspace": "3.8.1-next-1741253659.0",
134 | "@opensumi/ide-workspace-edit": "3.8.1-next-1741253659.0",
135 | "@opensumi/tree-sitter-wasm": "1.1.2",
136 | "@vscode/spdlog": "^0.15.0",
137 | "buffer": "^6.0.3",
138 | "electron-updater": "6.3.9",
139 | "js-yaml": "^4.1.0",
140 | "koa": "^2.15.3",
141 | "koa-static": "^5.0.0",
142 | "mri": "^1.2.0",
143 | "process": "^0.11.10",
144 | "react": "^18.3.1",
145 | "react-dom": "^18.3.1"
146 | },
147 | "packageManager": "yarn@4.3.1",
148 | "resolutions": {
149 | "@rjsf/validator-ajv6": "5.4.0"
150 | },
151 | "dependenciesMeta": {
152 | "@parcel/watcher": {
153 | "built": false
154 | },
155 | "@vscode/spdlog": {
156 | "built": false
157 | },
158 | "keytar": {
159 | "built": false
160 | },
161 | "node-pty": {
162 | "built": false
163 | },
164 | "nsfw": {
165 | "built": false
166 | },
167 | "spdlog": {
168 | "built": false
169 | }
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/ai/browser/ai-model.contribution.ts:
--------------------------------------------------------------------------------
1 | import { Autowired } from '@opensumi/di'
2 | import { AI_NATIVE_SETTING_GROUP_ID, localize, MaybePromise, Delayer, CommandService, AINativeSettingSectionsId } from '@opensumi/ide-core-common';
3 | import { Domain, PreferenceContribution, PreferenceSchema, ClientAppContribution, IClientApp, PreferenceService, COMMON_COMMANDS, IPreferenceSettingsService } from '@opensumi/ide-core-browser'
4 | import { ISettingRegistry, SettingContribution } from '@opensumi/ide-preferences';
5 | import { AIModelServicePath, IAIModelServiceProxy, ModelSettingId } from '../common'
6 | import { OutputChannel } from '@opensumi/ide-output/lib/browser/output.channel';
7 | import { OutputService } from '@opensumi/ide-output/lib/browser/output.service';
8 | import { MessageService } from '@opensumi/ide-overlay/lib/browser/message.service';
9 |
10 | const ModelSettingIdKeys = Object.keys(ModelSettingId);
11 |
12 | const aiNativePreferenceSchema: PreferenceSchema = {
13 | properties: {
14 | [ModelSettingId.baseUrl]: {
15 | type: 'string',
16 | defaultValue: 'http://127.0.0.1:11434/v1',
17 | },
18 | [ModelSettingId.apiKey]: {
19 | type: 'string',
20 | },
21 | [ModelSettingId.codeModelName]: {
22 | type: 'string',
23 | description: localize('preference.ai.model.code.modelName.tooltip')
24 | },
25 | [ModelSettingId.codeSystemPrompt]: {
26 | type: 'string',
27 | },
28 | [ModelSettingId.codeMaxTokens]: {
29 | type: 'number',
30 | minimum: 0,
31 | defaultValue: 64,
32 | description: localize('preference.ai.model.maxTokens.description'),
33 | },
34 | [ModelSettingId.codeTemperature]: {
35 | type: 'string',
36 | defaultValue: '0.20',
37 | description: localize('preference.ai.model.temperature.description'),
38 | },
39 | [ModelSettingId.codePresencePenalty]: {
40 | type: 'string',
41 | // minimum: -2.0,
42 | // maximum: 2.0,
43 | defaultValue: '1',
44 | description: localize('preference.ai.model.presencePenalty.description'),
45 | },
46 | [ModelSettingId.codeFrequencyPenalty]: {
47 | type: 'string',
48 | // minimum: -2.0,
49 | // maximum: 2.0,
50 | defaultValue: '1',
51 | description: localize('preference.ai.model.frequencyPenalty.description'),
52 | },
53 | [ModelSettingId.codeTopP]: {
54 | type: 'string',
55 | // minimum: 0,
56 | // maximum: 1,
57 | defaultValue: '1',
58 | description: localize('preference.ai.model.topP.description'),
59 | },
60 | [ModelSettingId.codeFimTemplate]: {
61 | type: 'string',
62 | description: localize('preference.ai.model.code.fimTemplate.tooltip'),
63 | },
64 | },
65 | };
66 |
67 | @Domain(ClientAppContribution, PreferenceContribution, SettingContribution)
68 | export class AIModelContribution implements PreferenceContribution, SettingContribution, ClientAppContribution {
69 | schema = aiNativePreferenceSchema;
70 |
71 | @Autowired(PreferenceService)
72 | private readonly preferenceService: PreferenceService;
73 |
74 | @Autowired(AIModelServicePath)
75 | modelService: IAIModelServiceProxy
76 |
77 | @Autowired(MessageService)
78 | messageService: MessageService;
79 |
80 | @Autowired(OutputService)
81 | outputService: OutputService;
82 |
83 | @Autowired(CommandService)
84 | commandService: CommandService
85 |
86 | @Autowired(IPreferenceSettingsService)
87 | preferenceSettingsService: IPreferenceSettingsService
88 |
89 | #output: OutputChannel
90 |
91 | get output() {
92 | if (!this.#output) {
93 | this.#output = this.outputService.getChannel('AI Native')
94 | }
95 | return this.#output
96 | }
97 |
98 | onDidStart(app: IClientApp): MaybePromise {
99 | const delayer = new Delayer(100);
100 | const values: Record = {}
101 | ModelSettingIdKeys.forEach((idKey) => {
102 | values[idKey] = this.preferenceService.getValid(ModelSettingId[idKey])
103 | this.preferenceService.onSpecificPreferenceChange(ModelSettingId[idKey], (change) => {
104 | values[idKey] = change.newValue
105 | delayer.trigger(() => this.setModeConfig(values))
106 | })
107 | })
108 | delayer.trigger(() => this.setModeConfig(values));
109 | this.checkModelConfig();
110 | }
111 |
112 | registerSetting(registry: ISettingRegistry): void {
113 | registry.registerSettingSection(AI_NATIVE_SETTING_GROUP_ID, {
114 | title: localize('preference.ai.model.title'),
115 | preferences: [
116 | {
117 | id: ModelSettingId.baseUrl,
118 | localized: 'preference.ai.model.baseUrl',
119 | },
120 | {
121 | id: ModelSettingId.apiKey,
122 | localized: 'preference.ai.model.apiKey',
123 | },
124 | {
125 | id: ModelSettingId.codeModelName,
126 | localized: 'preference.ai.model.code.modelName',
127 | },
128 | {
129 | id: ModelSettingId.codeSystemPrompt,
130 | localized: 'preference.ai.model.code.systemPrompt',
131 | },
132 | {
133 | id: ModelSettingId.codeMaxTokens,
134 | localized: 'preference.ai.model.code.maxTokens',
135 | },
136 | {
137 | id: ModelSettingId.codeTemperature,
138 | localized: 'preference.ai.model.code.temperature',
139 | },
140 | {
141 | id: ModelSettingId.codePresencePenalty,
142 | localized: 'preference.ai.model.code.presencePenalty',
143 | },
144 | {
145 | id: ModelSettingId.codeFrequencyPenalty,
146 | localized: 'preference.ai.model.code.frequencyPenalty',
147 | },
148 | {
149 | id: ModelSettingId.codeTopP,
150 | localized: 'preference.ai.model.code.topP',
151 | },
152 | {
153 | id: ModelSettingId.codeFimTemplate,
154 | localized: 'preference.ai.model.code.fimTemplate',
155 | },
156 | ],
157 | });
158 | }
159 |
160 | private async checkModelConfig() {
161 | const requirePreference = [
162 | AINativeSettingSectionsId.DeepseekApiKey,
163 | AINativeSettingSectionsId.OpenaiApiKey,
164 | AINativeSettingSectionsId.AnthropicApiKey,
165 | ];
166 |
167 | const hasRequirePreference = requirePreference.some(preference => !!this.preferenceService.getValid(preference));
168 | if (!hasRequirePreference) {
169 | this.preferenceService.has(AINativeSettingSectionsId.DeepseekApiKey);
170 | const res = await this.messageService.info(localize('ai.model.noConfig'), [
171 | localize('ai.model.go')
172 | ]);
173 | if (res === localize('ai.model.go')) {
174 | await this.commandService.executeCommand(COMMON_COMMANDS.OPEN_PREFERENCES.id)
175 | this.preferenceSettingsService.scrollToPreference(AINativeSettingSectionsId.LLMModelSelection);
176 | }
177 | }
178 | }
179 |
180 | private setModeConfig(values: Record) {
181 | this.modelService.setConfig(values)
182 | this.output.appendLine(`model config: ${JSON.stringify(values, null, 2)}`)
183 | }
184 | }
185 |
--------------------------------------------------------------------------------