├── .dockerignore ├── .env.sample ├── .github ├── dependabot.yml ├── renovate.json └── workflows │ └── package.yml ├── .gitignore ├── .yarn └── releases │ └── yarn-4.3.1.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── Dockerfile ├── LEGAL.md ├── LICENSE ├── README.md ├── assets ├── app-update.yml └── icon │ ├── 1024.png │ ├── 256.png │ ├── 512.png │ ├── icon.icns │ └── icon.ico ├── build ├── build-asar.ts ├── deps.ts ├── download-extensions.ts ├── extensions.json ├── icon.mjs ├── maker-nisi.ts ├── maker-zip.ts ├── rebuild.ts ├── util.ts ├── webpack-web │ ├── web-start.ts │ ├── webpack.base.config.ts │ ├── webpack.browser.config.ts │ ├── webpack.config.ts │ ├── webpack.ext-host.config.ts │ ├── webpack.node.config.ts │ ├── webpack.webview.config.ts │ └── webpack.worker-host.config.ts └── webpack │ ├── ForgeWebpackPlugin.ts │ ├── webpack.base.config.ts │ ├── webpack.ext-host.config.ts │ ├── webpack.main.config.ts │ ├── webpack.node.config.ts │ ├── webpack.renderer.config.ts │ ├── webpack.watcher-host.config.ts │ └── webpack.webview.config.ts ├── forge.config.ts ├── package.json ├── product.json ├── public └── index.html ├── src ├── ai │ ├── browser │ │ ├── ai-model.contribution.ts │ │ ├── ai-native.contribution.ts │ │ ├── ai-run.contribution.ts │ │ ├── ai-run.service.ts │ │ ├── ai-terminal-debug.service.ts │ │ ├── assets │ │ │ └── hi.png │ │ ├── command │ │ │ ├── command-prompt-manager.ts │ │ │ ├── command-render.module.less │ │ │ ├── command-render.tsx │ │ │ └── command.service.ts │ │ ├── components │ │ │ └── left-toolbar.tsx │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── inline-chat-operation.ts │ │ └── prompt.ts │ ├── common │ │ ├── index.ts │ │ └── types.ts │ └── node │ │ ├── ai-back.service.ts │ │ ├── index.ts │ │ ├── model.service.ts │ │ ├── pty │ │ ├── pty.service.ts │ │ └── shell-integration.ts │ │ ├── shell-integration.ts │ │ └── types.ts ├── auto-updater │ ├── browser │ │ ├── index.ts │ │ └── update.contribution.ts │ ├── common │ │ └── index.ts │ ├── electron-main │ │ ├── auto-updater.service.ts │ │ ├── index.ts │ │ ├── update-window.ts │ │ ├── update.contribution.ts │ │ ├── update.provider.ts │ │ └── update.service.ts │ └── update-window │ │ ├── UpdateView.tsx │ │ ├── index.html │ │ ├── index.tsx │ │ └── style.module.less ├── bootstrap-web │ ├── browser │ │ ├── common-modules.ts │ │ ├── core-commands.ts │ │ ├── index.ts │ │ ├── layout-config.ts │ │ ├── main.less │ │ ├── render-app.ts │ │ └── styles.less │ ├── common │ │ └── index.ts │ ├── ext-host │ │ ├── index.ts │ │ └── index.worker.ts │ └── node │ │ ├── common-modules.ts │ │ ├── index.ts │ │ └── start-server.ts ├── bootstrap │ ├── browser │ │ ├── index.html │ │ ├── index.less │ │ ├── index.ts │ │ └── preload.js │ ├── electron-main │ │ └── index.ts │ ├── ext-host │ │ ├── index.ts │ │ └── index.worker.ts │ ├── node │ │ └── index.ts │ └── watcher-host │ │ └── index.ts ├── core │ ├── browser │ │ ├── assets │ │ │ └── logo.svg │ │ ├── header │ │ │ ├── header.contribution.ts │ │ │ ├── header.module.less │ │ │ └── header.view.tsx │ │ ├── index.ts │ │ ├── menu.contribution.ts │ │ ├── patch.ts │ │ ├── project.contribution.ts │ │ ├── theme.contribution.ts │ │ └── welcome │ │ │ ├── common.ts │ │ │ ├── welcome.component.tsx │ │ │ ├── welcome.contribution.ts │ │ │ └── welcome.module.less │ ├── common │ │ ├── asar.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ └── types.ts │ ├── electron-main │ │ ├── app.ts │ │ ├── environment.service.ts │ │ ├── index.ts │ │ ├── lifecycle.contribution.ts │ │ ├── menu.contribution.ts │ │ ├── module.ts │ │ ├── storage.service.ts │ │ ├── theme.service.ts │ │ ├── types.ts │ │ ├── window │ │ │ ├── window-lifecycle.ts │ │ │ ├── window.contribution.ts │ │ │ └── windows-manager.ts │ │ └── workspace │ │ │ └── workspace-history.contribution.ts │ └── node │ │ └── index.ts ├── i18n │ ├── en-US.ts │ ├── index.ts │ └── zh-CN.ts └── logger │ ├── common │ ├── index.ts │ ├── log-manager.ts │ ├── log-service.ts │ └── types.ts │ ├── electron-main │ ├── index.ts │ └── log-manager.ts │ └── node │ ├── index.ts │ └── log-manager.ts ├── tsconfig.json ├── typings └── global │ └── index.d.ts └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | CODE_WINDOW_CLIENT_ID=CODE_WINDOW_CLIENT_ID 2 | IDE_LOG_HOME="" 3 | IDE_SERVER_PORT=8000 4 | WS_PATH=ws://localhost:8080 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | - package-ecosystem: 'npm' 8 | directory: '/' 9 | schedule: 10 | interval: 'weekly' 11 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "timezone": "Asia/Shanghai", 5 | "enabledManagers": ["npm"], 6 | "groupName": "opensumi packages", 7 | "packageRules": [ 8 | { 9 | "packagePatterns": ["*"], 10 | "excludePackagePatterns": ["^@opensumi/ide-"], 11 | "enabled": false 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: Package 2 | 3 | # Cancel prev CI if new commit come 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 6 | cancel-in-progress: true 7 | 8 | on: 9 | workflow_dispatch: 10 | push: 11 | branches: 12 | - main 13 | - v*.* 14 | pull_request: 15 | branches: 16 | - main 17 | - v*.* 18 | paths: 19 | - 'packages/**' 20 | - package.json 21 | - yarn.lock 22 | 23 | jobs: 24 | build: 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | matrix: 28 | os: [macos-latest] 29 | node-version: [20.x] 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Use Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | - name: Get yarn cache directory path 38 | id: yarn_cache_dir_path 39 | run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT 40 | 41 | - uses: actions/cache@v4 42 | id: yarn_cache 43 | with: 44 | path: ${{ steps.yarn_cache_dir_path.outputs.dir }} 45 | key: ${{ runner.os }}-yarn-${{ hashFiles('./yarn.lock') }} 46 | restore-keys: | 47 | ${{ runner.os }}-yarn- 48 | 49 | - name: Install 50 | run: | 51 | yarn install --immutable 52 | 53 | - name: Build 54 | run: | 55 | yarn run electron-rebuild 56 | 57 | - name: Package 58 | run: | 59 | yarn run package 60 | 61 | build-windows: 62 | runs-on: windows-2019 63 | steps: 64 | - uses: actions/checkout@v4 65 | - name: Use Node.js 20.x 66 | uses: actions/setup-node@v4 67 | with: 68 | node-version: 20.x 69 | - name: Get yarn cache directory path 70 | id: yarn_cache_dir_path 71 | run: echo "dir=$(yarn config get cacheFolder)" >> $Env:GITHUB_OUTPUT 72 | 73 | - uses: actions/cache@v4 74 | id: yarn_cache 75 | with: 76 | path: ${{ steps.yarn_cache_dir_path.outputs.dir }} 77 | key: ${{ runner.os }}-yarn-${{ hashFiles('./yarn.lock') }} 78 | restore-keys: | 79 | ${{ runner.os }}-yarn- 80 | - name: Install 81 | run: | 82 | yarn install --immutable 83 | 84 | - name: Build 85 | run: | 86 | yarn run electron-rebuild 87 | 88 | - name: Package 89 | run: | 90 | yarn run package 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 由 https://github.com/msfeldstein/gitignore 自动生成 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | # typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless/ 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # DynamoDB Local files 80 | .dynamodb/ 81 | dist 82 | lib 83 | out 84 | out-web 85 | .vscode/* 86 | !.vscode/launch.json 87 | 88 | tools/workspace/* 89 | 90 | packages/feature-extension/test/init/node_modules 91 | packages/vscode-extension/test/init/node_modules 92 | 93 | extensions 94 | app 95 | 96 | out-x64 97 | out-arm64 98 | 99 | .DS_Store 100 | .idea 101 | .node 102 | package-lock.json 103 | .pnp.* 104 | .yarn/* 105 | !.yarn/patches 106 | !.yarn/plugins 107 | !.yarn/releases 108 | !.yarn/sdks 109 | !.yarn/versions 110 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.3.1.cjs 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20 AS builder 2 | 3 | ENV WORKSPACE_DIR=/workspace 4 | ENV EXTENSION_DIR=/extensions 5 | ENV NODE_ENV=production 6 | ENV WS_PATH=ws://localhost:8000 7 | 8 | RUN mkdir -p ${WORKSPACE_DIR} &&\ 9 | mkdir -p ${EXTENSION_DIR} 10 | 11 | RUN apt-get update && apt-get install -y libsecret-1-dev 12 | 13 | RUN npm config set registry https://registry.npmmirror.com 14 | 15 | # 设置工作目录 16 | WORKDIR /build 17 | 18 | COPY . /build 19 | 20 | # 清理全局安装的包并安装 yarn 21 | RUN npm cache clean --force && \ 22 | rm -rf /usr/local/lib/node_modules/yarn* && \ 23 | rm -rf /usr/local/bin/yarn* && \ 24 | npm install -g yarn 25 | 26 | # 配置yarn为国内源 27 | RUN yarn config set npmRegistryServer https://registry.npmmirror.com 28 | 29 | # 安装依赖$构建项目 30 | RUN yarn install && \ 31 | yarn run build-web && \ 32 | yarn run web-rebuild 33 | 34 | FROM node:20 AS app 35 | 36 | ENV WORKSPACE_DIR=/workspace 37 | ENV EXTENSION_DIR=/root/.sumi/extensions 38 | 39 | RUN mkdir -p ${WORKSPACE_DIR} &&\ 40 | mkdir -p ${EXTENSION_DIR} &&\ 41 | mkdir -p /extensions 42 | 43 | # 设置工作目录 44 | WORKDIR /release 45 | 46 | COPY --from=builder /build/out /release/out 47 | COPY --from=builder /build/node_modules /release/node_modules 48 | 49 | EXPOSE 8000 50 | 51 | CMD [ "node", "./out/node/index.js"] -------------------------------------------------------------------------------- /LEGAL.md: -------------------------------------------------------------------------------- 1 | Legal Disclaimer 2 | 3 | Within this source code, the comments in Chinese shall be the original, governing version. Any comment in other languages are for reference only. In the event of any conflict between the Chinese language version comments and other language version comments, the Chinese language version shall prevail. 4 | 5 | 法律免责声明 6 | 7 | 关于代码注释部分,中文注释为官方版本,其它语言注释仅做参考。中文注释可能与其它语言注释存在不一致,当中文注释与其它语言注释存在不一致时,请以中文注释为准。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

CodeFuse IDE

2 | 3 |

AI Native IDE based on CodeFuse and OpenSumi.

4 | 5 | ![0F2230D7-7623-4141-91BE-487973ED0AF7](https://github.com/user-attachments/assets/8b6c71c2-7242-4894-9c73-996365b4245a) 6 | 7 | 8 | [![Discussions][discussions-image]][discussions-url] [![Open in CodeBlitz][codeblitz-image]][codeblitz-url] 9 | 10 | [discussions-image]: https://img.shields.io/badge/discussions-on%20github-blue 11 | [discussions-url]: https://github.com/codefuse-ai/codefuse-ide/discussions 12 | [codeblitz-image]: https://img.shields.io/badge/Ant_Codespaces-Open_in_CodeBlitz-1677ff 13 | [codeblitz-url]: https://codeblitz.cloud.alipay.com/github/codefuse-ai/codefuse-ide 14 | [github-issues-url]: https://github.com/opensumi/core/issues 15 | [help-wanted-image]: https://flat.badgen.net/github/label-issues/codefuse-ai/codefuse-ide/🤔%20help%20wanted/open 16 | [help-wanted-url]: https://github.com/codefuse-ai/codefuse-ide/issues?q=is%3Aopen+is%3Aissue+label%3A%22🤔+help+wanted%22 17 | 18 | ## ✨ Features 19 | - **AI-Native Development Environment**: Enjoy an integrated development environment that leverages AI technologies to enhance productivity and streamline workflows. 20 | - **Open Model Integration**: Our platform supports the seamless integration of various models, allowing developers to customize and extend functionality according to their needs. 21 | - **VS Code Extension Compatibility**: Benefit from a rich ecosystem of plugins by ensuring compatibility with VS Code extensions, enabling you to leverage existing tools and resources. 22 | - **Complete Solution**: Uses electron-forge to package desktop applications and supports development, building, packaging, and auto updates. 23 | 24 | ## Getting started 25 | 26 | See https://github.com/codefuse-ai/codefuse-ide/releases 27 | 28 | ## Contributing 29 | 30 | ### Preparation 31 | - install Node.js >= 20 32 | - you can use npmmirror.com to speed up the installation in china 33 | - `yarn config set -H npmRegistryServer "https://registry.npmmirror.com"` 34 | - `export ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/` 35 | 36 | ### Start the project 37 | ```bash 38 | # install dependencies 39 | yarn 40 | # rebuild native dependencies for electron 41 | yarn run electron-rebuild 42 | # start project 43 | yarn run start 44 | ``` 45 | 46 | ### Start the web project (experimental) 47 | ```bash 48 | # install dependencies 49 | yarn 50 | # rebuild native dependencies for web 51 | yarn run web-rebuild 52 | # build web 53 | yarn run build-web 54 | # start project, visit http://localhost:8080 or http://localhost:8080/?workspaceDir=workspace_dir 55 | yarn run start-web 56 | ``` 57 | 58 | ## Links 59 | 60 | - **CodeFuse**: https://codefuse.ai 61 | - **OpenSumi**: https://opensumi.com 62 | -------------------------------------------------------------------------------- /assets/app-update.yml: -------------------------------------------------------------------------------- 1 | provider: generic 2 | url: '' 3 | channel: latest 4 | updaterCacheDirName: CodeFuseIDE-updater 5 | -------------------------------------------------------------------------------- /assets/icon/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefuse-ai/codefuse-ide/2f4cfb25c8f8a04f540287aa38f72e22813cee7c/assets/icon/1024.png -------------------------------------------------------------------------------- /assets/icon/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefuse-ai/codefuse-ide/2f4cfb25c8f8a04f540287aa38f72e22813cee7c/assets/icon/256.png -------------------------------------------------------------------------------- /assets/icon/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefuse-ai/codefuse-ide/2f4cfb25c8f8a04f540287aa38f72e22813cee7c/assets/icon/512.png -------------------------------------------------------------------------------- /assets/icon/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefuse-ai/codefuse-ide/2f4cfb25c8f8a04f540287aa38f72e22813cee7c/assets/icon/icon.icns -------------------------------------------------------------------------------- /assets/icon/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefuse-ai/codefuse-ide/2f4cfb25c8f8a04f540287aa38f72e22813cee7c/assets/icon/icon.ico -------------------------------------------------------------------------------- /build/build-asar.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process' 2 | import * as path from 'node:path' 3 | import * as fs from 'node:fs/promises' 4 | import { glob } from 'glob' 5 | import { createPackageWithOptions } from 'asar'; 6 | import { asarDeps } from './deps' 7 | 8 | interface Dep { 9 | name: string; 10 | version: string; 11 | } 12 | 13 | export async function buildAsar(destDir: string) { 14 | const deps = getAllAsarDeps() 15 | await fs.rm(destDir, { recursive: true, force: true }) 16 | const srcModules = path.join(process.cwd(), 'node_modules') 17 | const destModules = path.join(destDir, 'node_modules') 18 | await copyDeps(srcModules, destModules, deps) 19 | await createPackageWithOptions( 20 | destModules, 21 | path.join(destDir, 'node_modules.asar'), 22 | { 23 | dot: true, 24 | unpack: '{' + [ 25 | '**/*.node', 26 | '**/@opensumi/vscode-ripgrep/bin/*', 27 | '**/node-pty/build/Release/*', 28 | '**/node-pty/lib/worker/conoutSocketWorker.js', 29 | '**/node-pty/lib/shared/conout.js', 30 | '**/*.wasm', 31 | ].join(',') + '}' 32 | } 33 | ); 34 | await fs.rm(destModules, { recursive: true }) 35 | } 36 | 37 | function parseSemver(value) { 38 | const [, name, version] = value.match(/(@?[^@]+)@(?:.+):(.+)/); 39 | return { name, version } 40 | } 41 | 42 | function getAllAsarDeps() { 43 | const raw = execSync('corepack yarn info -A -R --json', { encoding: 'utf-8' }) 44 | const asarDepsMap = {}; 45 | const result: Dep[] = []; 46 | const allDeps = raw 47 | .split('\n') 48 | .filter(Boolean) 49 | .map(line => JSON.parse(line)) 50 | .reduce((acc, data) => { 51 | const { name } = parseSemver(data.value); 52 | if (asarDeps.includes(name)) { 53 | if (asarDepsMap[name]) { 54 | throw new Error(`Duplicate package: ${name}`) 55 | } 56 | asarDepsMap[name] = data.value 57 | } 58 | acc[data.value] = data 59 | return acc 60 | }, {}); 61 | 62 | const addDep = (value) => { 63 | const { name, version } = parseSemver(value) 64 | if (name === 'node-gyp') return 65 | result.push({ name, version }) 66 | const dependencies = allDeps[value].children.Dependencies 67 | if (!dependencies) return 68 | dependencies.forEach(({ locator }) => { 69 | const { name, version } = parseSemver(locator) 70 | addDep(`${name}@npm:${version}`) 71 | }) 72 | } 73 | 74 | asarDeps.forEach((pkgName) => { 75 | const value = asarDepsMap[pkgName] 76 | addDep(value) 77 | }) 78 | 79 | return result 80 | } 81 | 82 | async function copyDeps(srcModules: string, destModules: string, depList: Dep[]) { 83 | const filenames = await Promise.all([ 84 | glob(depList.map(dep => `${dep.name}/**`), { 85 | cwd: srcModules, 86 | dot: true, 87 | nodir: true, 88 | ignore: [ 89 | '**/package-lock.json', 90 | '**/yarn.lock', 91 | '**/*.js.map', 92 | 'nan/**', 93 | '*/node_modules/nan/**', 94 | '**/docs/**', 95 | '**/example/**', 96 | '**/examples/**', 97 | '**/test/**', 98 | '**/tests/**', 99 | '**/.vscode/**', 100 | '**/node-addon-api/**/*', 101 | '**/prebuild-install/**/*', 102 | '**/History.md', 103 | '**/CHANGELOG.md', 104 | '**/README.md', 105 | '**/readme.md', 106 | '**/readme.markdown', 107 | '**/CODE_OF_CONDUCT.md', 108 | '**/SUPPORT.md', 109 | '**/CONTRIBUTING.md', 110 | '**/*.ts', 111 | '@vscode/spdlog/binding.gyp', 112 | '@vscode/spdlog/build/**', 113 | '@vscode/spdlog/deps/**', 114 | '@vscode/spdlog/src/**', 115 | '@vscode/spdlog/*.yml', 116 | 'node-pty/binding.gyp', 117 | 'node-pty/build/**', 118 | 'node-pty/src/**', 119 | 'node-pty/lib/*.test.js', 120 | 'node-pty/tools/**', 121 | 'node-pty/deps/**', 122 | 'node-pty/scripts/**', 123 | '@parcel/watcher/binding.gyp', 124 | '@parcel/watcher/build/**', 125 | '@parcel/watcher/prebuilds/**', 126 | '@parcel/watcher/src/**', 127 | 'nsfw/binding.gyp', 128 | 'nsfw/build/**', 129 | 'nsfw/includes/**', 130 | 'nsfw/src/**', 131 | 'keytar/binding.gyp', 132 | 'keytar/build/**', 133 | 'keytar/src/**', 134 | ] 135 | }), 136 | glob([ 137 | '@vscode/spdlog/build/Release/*.node', 138 | 'node-pty/build/Release/spawn-helper', 139 | 'node-pty/build/Release/*.exe', 140 | 'node-pty/build/Release/*.dll', 141 | 'node-pty/build/Release/*.node', 142 | '@parcel/watcher/build/Release/*.node', 143 | 'nsfw/build/Release/*.node', 144 | 'keytar/build/Release/*.node', 145 | ], { 146 | cwd: srcModules, 147 | dot: true, 148 | nodir: true, 149 | }) 150 | ]) 151 | await fs.rm(destModules, { recursive: true, force: true }) 152 | 153 | for (const filename of filenames.flat(Infinity) as string[]) { 154 | const destPath = path.join(destModules, filename) 155 | await fs.mkdir(path.dirname(destPath), { recursive: true }) 156 | await fs.copyFile(path.join(srcModules, filename), destPath) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /build/deps.ts: -------------------------------------------------------------------------------- 1 | export const nativeDeps = [ 2 | '@parcel/watcher', 3 | '@vscode/spdlog', 4 | 'node-pty', 5 | 'nsfw', 6 | 'spdlog', 7 | 'keytar', 8 | ] 9 | 10 | export const postInstallDeps = [ 11 | '@opensumi/vscode-ripgrep', 12 | ] 13 | 14 | export const asarDeps = [ 15 | ...nativeDeps, 16 | ...postInstallDeps, 17 | 'vscode-oniguruma', 18 | '@opensumi/tree-sitter-wasm' 19 | ] 20 | -------------------------------------------------------------------------------- /build/icon.mjs: -------------------------------------------------------------------------------- 1 | import { appBuilderPath } from 'app-builder-bin'; 2 | import { spawnSync } from 'child_process' 3 | import { join } from 'path' 4 | 5 | for (const format of ['icns', 'ico']) { 6 | const { error } = spawnSync(appBuilderPath, [ 7 | 'icon', 8 | '--format', 9 | format, 10 | '--input', 11 | join(import.meta.dirname, '../assets/icon/1024.png'), 12 | '--out', 13 | join(import.meta.dirname, '../assets/icon'), 14 | ], { 15 | stdio: 'inherit' 16 | }) 17 | if (error) { 18 | console.error(error) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /build/maker-nisi.ts: -------------------------------------------------------------------------------- 1 | import { MakerBase, MakerOptions } from '@electron-forge/maker-base'; 2 | import { ForgePlatform } from '@electron-forge/shared-types'; 3 | import { buildForge } from 'app-builder-lib' 4 | import path from 'node:path' 5 | import { signWinApp } from './util' 6 | import { productName, applicationName } from '../product.json' 7 | 8 | interface MakerNsisConfig {} 9 | 10 | export default class MakerNsis extends MakerBase { 11 | name = 'nsis'; 12 | 13 | defaultPlatforms: ForgePlatform[] = ['win32']; 14 | 15 | isSupportedOnCurrentPlatform(): boolean { 16 | return true; 17 | } 18 | 19 | async make({ dir, makeDir, targetArch }: MakerOptions): Promise { 20 | return buildForge( 21 | { dir }, 22 | { 23 | win: [`nsis:${targetArch}`], 24 | prepackaged: dir, 25 | config: { 26 | productName, 27 | artifactName: `${applicationName}Setup-\${os}-\${arch}.\${ext}`, 28 | directories: { 29 | output: path.join(makeDir, 'nsis', targetArch), 30 | }, 31 | win: { 32 | sign: ({ path }) => signWinApp(path) 33 | }, 34 | nsis: { 35 | oneClick: false, 36 | allowToChangeInstallationDirectory: true, 37 | perMachine: true, 38 | installerIcon: path.join(__dirname, '../assets/icon/icon.ico') 39 | }, 40 | publish: { 41 | provider: 'generic', 42 | url: '', 43 | channel: 'latest' 44 | } 45 | }, 46 | } 47 | ) 48 | } 49 | } 50 | 51 | export { MakerNsis }; 52 | -------------------------------------------------------------------------------- /build/maker-zip.ts: -------------------------------------------------------------------------------- 1 | import { MakerBase, MakerOptions } from '@electron-forge/maker-base'; 2 | import { ForgePlatform } from '@electron-forge/shared-types'; 3 | import { build } from 'app-builder-lib' 4 | import path from 'node:path' 5 | import { productName, applicationName } from '../product.json' 6 | 7 | interface MakerZipConfig {} 8 | 9 | export default class MakerZip extends MakerBase { 10 | name = 'zip'; 11 | 12 | defaultPlatforms: ForgePlatform[] = ['darwin']; 13 | 14 | isSupportedOnCurrentPlatform(): boolean { 15 | return true; 16 | } 17 | 18 | async make({ dir, makeDir, targetArch, appName }: MakerOptions): Promise { 19 | return build( 20 | { 21 | prepackaged: path.resolve(dir, `${appName}.app`), 22 | mac: [`zip:${targetArch}`], 23 | config: { 24 | productName, 25 | artifactName: `${applicationName}-\${os}-\${arch}.\${ext}`, 26 | directories: { 27 | output: path.join(makeDir, 'zip', targetArch), 28 | }, 29 | publish: { 30 | provider: 'generic', 31 | url: '', 32 | channel: 'latest' 33 | } 34 | }, 35 | } 36 | ) 37 | } 38 | } 39 | 40 | export { MakerZip }; 41 | -------------------------------------------------------------------------------- /build/rebuild.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { version as electronVersion } from 'electron/package.json' 3 | import { nativeDeps, postInstallDeps } from './deps' 4 | import { exec } from './util' 5 | import { parseArgv } from '@opensumi/ide-utils/lib/argv' 6 | 7 | const argv = parseArgv(process.argv) 8 | 9 | export const rebuild = async (config?: { arch?: string, cwd?: string, silent?: boolean, loglevel?: string }) => { 10 | const target = argv.target || 'electron' 11 | const arch = config?.arch || process.arch 12 | const cwd = config?.cwd || process.cwd() 13 | const loglevel = config?.loglevel || 'info' 14 | 15 | for (const pkgName of nativeDeps) { 16 | const pkgPath = path.join(cwd, 'node_modules', pkgName); 17 | await exec( 18 | [ 19 | 'npx', 20 | 'node-gyp', 21 | 'rebuild', 22 | ...target == 'electron' ? [ 23 | '--runtime=electron', 24 | `--target=${electronVersion}`, 25 | `--arch=${arch}`, 26 | `--dist-url=https://electronjs.org/headers`, 27 | `--loglevel=${loglevel}` 28 | ] : [] 29 | ].join(' '), 30 | null, 31 | { 32 | cwd: pkgPath, 33 | stdio: config?.silent ? 'ignore' : 'inherit', 34 | }) 35 | } 36 | 37 | for (const pkgName of postInstallDeps) { 38 | const pkgPath = path.join(process.cwd(), 'node_modules', pkgName); 39 | await exec( 40 | `npm run postinstall --arch=${arch} -- --force`, 41 | null, 42 | { 43 | cwd: pkgPath, 44 | stdio: config?.silent ? 'ignore' : 'inherit' 45 | } 46 | ) 47 | } 48 | } 49 | 50 | if (require.main === module) { 51 | rebuild({ 52 | silent: false 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /build/util.ts: -------------------------------------------------------------------------------- 1 | import { spawn, spawnSync, SpawnOptions } from 'child_process' 2 | 3 | export const exec = async (command: string, args?: string[] | null, options?: SpawnOptions) => { 4 | await new Promise((resolve, reject) => { 5 | const child = spawn( 6 | command, 7 | args || [], 8 | { 9 | stdio: 'inherit', 10 | shell: true, 11 | ...options, 12 | } 13 | ) 14 | 15 | let exited = false; 16 | let err: Error | null = null; 17 | const handleExit = (code: number | null, signal: string | null) => { 18 | if (exited) return; 19 | exited = true; 20 | 21 | if (!err && code === 0 && signal === null) { 22 | resolve(); 23 | return; 24 | } 25 | 26 | reject(err || new Error(`exec command: '${command}' error, code: ${code}, signal: ${signal}`)) 27 | } 28 | child.on('error', e => { 29 | err = e 30 | handleExit(null, null) 31 | }) 32 | child.on('exit', handleExit) 33 | }) 34 | } 35 | 36 | export const execSync = async (command: string, args?: string[] | null, options?: SpawnOptions) => { 37 | const { error } = spawnSync( 38 | command, 39 | args || [], 40 | { 41 | stdio: 'inherit', 42 | shell: true, 43 | ...options, 44 | } 45 | ) 46 | if (error) { 47 | throw error 48 | } 49 | } 50 | 51 | export const signWinApp = async (file: string) => { 52 | const signPath = process.env.WINDOWS_SIGN_TOOL_PATH 53 | if (!signPath) return Promise.resolve() 54 | return exec(signPath, [file, file], { shell: false }) 55 | } 56 | -------------------------------------------------------------------------------- /build/webpack-web/web-start.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import path from 'path'; 3 | 4 | // 定义每个命令的启动函数 5 | const commands = { 6 | client: 'cross-env NODE_ENV=development WEBPACK_LOG_LEVEL=info webpack-dev-server --client-logging info --config ./build/webpack-web/webpack.browser.config.ts --progress --color', 7 | webview: 'cross-env NODE_ENV=development webpack-dev-server --config ./build/webpack-web/webpack.webview.config.ts --progress --color', 8 | server: 'cross-env NODE_ENV=development node -r ts-node/register -r tsconfig-paths/register src/bootstrap-web/node/index.ts' 9 | }; 10 | 11 | // 启动子进程并打印输出 12 | function startProcess(command: string, name: string) { 13 | const child = exec(command, {cwd: path.resolve(__dirname, '../../')}, (error, stdout, stderr) => { 14 | if (error) { 15 | console.error(`[${name}] Error: ${error.message}`); 16 | return; 17 | } 18 | 19 | if (stderr) { 20 | console.error(`[${name}] stderr: ${stderr}`); 21 | return; 22 | } 23 | 24 | console.log(`[${name}] stdout: ${stdout}`); 25 | }); 26 | 27 | child.stdout?.on('data', (data) => { 28 | console.log(`[${name}] ${data.toString()}`); 29 | }); 30 | 31 | child.stderr?.on('data', (data) => { 32 | console.error(`[${name}] ${data.toString()}`); 33 | }); 34 | } 35 | 36 | // 启动所有进程 37 | function startAll() { 38 | Object.entries(commands).forEach(([name, command]) => { 39 | startProcess(command, name); 40 | }); 41 | } 42 | 43 | startAll(); -------------------------------------------------------------------------------- /build/webpack-web/webpack.base.config.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, DefinePlugin } from 'webpack' 2 | import path from 'node:path' 3 | import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin' 4 | import { merge } from 'webpack-merge' 5 | 6 | export const webpackDir = path.resolve(path.join(__dirname,'..','..', 'out')) 7 | 8 | export const devServerPort = 8080 9 | 10 | export const codeWindowName = 'code' 11 | 12 | export const updateWindowName = 'update' 13 | 14 | export const createConfig = (config: Configuration | ((_env: unknown, argv: Record) => Configuration)) => (_env: unknown, argv: Record) => { 15 | return merge({ 16 | mode: argv.mode, 17 | devtool: argv.mode === 'development' ? 'source-map': false, 18 | node: { 19 | __dirname: false, 20 | __filename: false, 21 | }, 22 | output: { 23 | asyncChunks: false, 24 | }, 25 | resolve: { 26 | extensions: ['.ts', '.tsx', '.mjs', '.js', '.json', '.less'], 27 | plugins: [ 28 | new TsconfigPathsPlugin({ 29 | configFile: path.join(__dirname, '../../tsconfig.json'), 30 | }), 31 | ], 32 | }, 33 | module: { 34 | // https://github.com/webpack/webpack/issues/196#issuecomment-397606728 35 | exprContextCritical: false, 36 | rules: [ 37 | { 38 | test: /\.tsx?$/, 39 | loader: 'ts-loader', 40 | exclude: /(node_modules|\.webpack)/, 41 | options: { 42 | configFile: path.join(__dirname, '../../tsconfig.json'), 43 | transpileOnly: true, 44 | }, 45 | }, 46 | { 47 | test: /\.mjs$/, 48 | include: /node_modules/, 49 | type: 'javascript/auto', 50 | }, 51 | ], 52 | }, 53 | plugins: [ 54 | new DefinePlugin({ 55 | 'process.env.KTLOG_SHOW_DEBUG': argv.mode === 'development', 56 | }), 57 | ], 58 | }, typeof config === 'function' ? config(_env, argv) : config); 59 | }; 60 | -------------------------------------------------------------------------------- /build/webpack-web/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import browser from './webpack.browser.config'; 2 | import webview from './webpack.webview.config'; 3 | import extHost from './webpack.ext-host.config'; 4 | import workerHost from './webpack.worker-host.config' 5 | import node from './webpack.node.config'; 6 | 7 | export default [browser, webview, extHost, workerHost, node]; -------------------------------------------------------------------------------- /build/webpack-web/webpack.ext-host.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { createConfig, webpackDir } from './webpack.base.config'; 3 | import { asarDeps } from '../deps' 4 | 5 | const srcDir = path.resolve('src/bootstrap-web/ext-host'); 6 | const outDir = path.join(webpackDir, 'ext-host'); 7 | 8 | export default createConfig((_, argv) => ({ 9 | entry: srcDir, 10 | output: { 11 | filename: 'index.js', 12 | path: outDir, 13 | }, 14 | externals: [ 15 | ({ request }, callback) => { 16 | if (asarDeps.includes(request!)) { 17 | return callback(null, 'commonjs ' + request); 18 | } 19 | callback(); 20 | }, 21 | ], 22 | target: 'node', 23 | })) 24 | -------------------------------------------------------------------------------- /build/webpack-web/webpack.node.config.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefuse-ai/codefuse-ide/2f4cfb25c8f8a04f540287aa38f72e22813cee7c/build/webpack-web/webpack.node.config.ts -------------------------------------------------------------------------------- /build/webpack-web/webpack.webview.config.ts: -------------------------------------------------------------------------------- 1 | import {webpackDir} from "./webpack.base.config"; 2 | 3 | const path = require('path'); 4 | const entry = require.resolve('@opensumi/ide-webview/lib/webview-host/web-preload.js'); 5 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); 8 | 9 | const tsConfigPath = path.join(__dirname, '../../tsconfig.json'); 10 | const distDir = path.join(webpackDir, 'webview'); 11 | const port = 8899; 12 | 13 | export default { 14 | entry, 15 | output: { 16 | filename: 'webview.js', 17 | path: distDir, 18 | clean: true, 19 | }, 20 | cache: { 21 | type: 'filesystem', 22 | }, 23 | resolve: { 24 | extensions: ['.ts', '.tsx', '.js', '.json', '.less'], 25 | plugins: [ 26 | new TsconfigPathsPlugin({ 27 | configFile: tsConfigPath, 28 | }), 29 | ], 30 | }, 31 | bail: true, 32 | mode: process.env.NODE_ENV || 'development', 33 | devtool: 'source-map', 34 | module: { 35 | // https://github.com/webpack/webpack/issues/196#issuecomment-397606728 36 | exprContextCritical: false, 37 | rules: [ 38 | { 39 | test: /\.tsx?$/, 40 | loader: 'ts-loader', 41 | options: { 42 | happyPackMode: true, 43 | transpileOnly: true, 44 | configFile: tsConfigPath, 45 | }, 46 | }, 47 | ], 48 | }, 49 | resolveLoader: { 50 | modules: [ 51 | path.join(__dirname, '../../../node_modules'), 52 | path.join(__dirname, '../../node_modules'), 53 | path.resolve('node_modules'), 54 | ], 55 | extensions: ['.ts', '.tsx', '.js', '.json', '.less'], 56 | mainFields: ['loader', 'main'], 57 | }, 58 | plugins: [ 59 | new HtmlWebpackPlugin({ 60 | template: path.dirname(entry) + '/webview.html', 61 | }), 62 | new NodePolyfillPlugin({ 63 | includeAliases: ['process', 'Buffer'], 64 | }), 65 | ], 66 | devServer: { 67 | static: { 68 | directory: path.join(__dirname , webpackDir) 69 | }, 70 | allowedHosts: 'all', 71 | port, 72 | host: "0.0.0.0", 73 | open: false, 74 | hot: true, 75 | client: { 76 | overlay: { 77 | errors: true, 78 | warnings: false, 79 | runtimeErrors: false, 80 | }, 81 | }, 82 | }, 83 | }; -------------------------------------------------------------------------------- /build/webpack-web/webpack.worker-host.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { ProvidePlugin } from 'webpack'; 3 | import {webpackDir} from "./webpack.base.config"; 4 | import {createConfig} from "../webpack/webpack.base.config"; 5 | const outDir = path.join(webpackDir, 'ext-host'); 6 | 7 | export default createConfig({ 8 | entry: require.resolve('@opensumi/ide-extension/lib/hosted/worker.host-preload'), 9 | output: { 10 | filename: 'worker-host.js', 11 | path: outDir, 12 | }, 13 | target: 'webworker', 14 | node: { 15 | global: true, 16 | }, 17 | resolve: { 18 | fallback: { 19 | os: false, 20 | util: false, 21 | buffer: require.resolve('buffer/'), 22 | }, 23 | }, 24 | plugins: [ 25 | new ProvidePlugin({ 26 | Buffer: ['buffer', 'Buffer'], 27 | process: 'process/browser', 28 | }), 29 | ], 30 | }) -------------------------------------------------------------------------------- /build/webpack/webpack.base.config.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, DefinePlugin } from 'webpack' 2 | import path from 'node:path' 3 | import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin' 4 | import { merge } from 'webpack-merge' 5 | 6 | export const webpackDir = path.resolve('out') 7 | 8 | export const devServerPort = 3000 9 | 10 | export const codeWindowName = 'code' 11 | 12 | export const updateWindowName = 'update' 13 | 14 | export const createConfig = (config: Configuration | ((_env: unknown, argv: Record) => Configuration)) => (_env: unknown, argv: Record) => { 15 | return merge({ 16 | mode: argv.mode, 17 | devtool: argv.mode === 'development' ? 'source-map': false, 18 | node: { 19 | __dirname: false, 20 | __filename: false, 21 | }, 22 | output: { 23 | asyncChunks: false, 24 | }, 25 | resolve: { 26 | extensions: ['.ts', '.tsx', '.mjs', '.js', '.json', '.less'], 27 | plugins: [ 28 | new TsconfigPathsPlugin({ 29 | configFile: path.join(__dirname, '../../tsconfig.json'), 30 | }), 31 | ], 32 | }, 33 | module: { 34 | // https://github.com/webpack/webpack/issues/196#issuecomment-397606728 35 | exprContextCritical: false, 36 | rules: [ 37 | { 38 | test: /\.tsx?$/, 39 | loader: 'ts-loader', 40 | exclude: /(node_modules|\.webpack)/, 41 | options: { 42 | configFile: path.join(__dirname, '../../tsconfig.json'), 43 | transpileOnly: true, 44 | }, 45 | }, 46 | { 47 | test: /\.mjs$/, 48 | include: /node_modules/, 49 | type: 'javascript/auto', 50 | }, 51 | ], 52 | }, 53 | plugins: [ 54 | new DefinePlugin({ 55 | 'process.env.KTLOG_SHOW_DEBUG': argv.mode === 'development', 56 | }), 57 | ], 58 | }, typeof config === 'function' ? config(_env, argv) : config); 59 | }; 60 | -------------------------------------------------------------------------------- /build/webpack/webpack.ext-host.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { ProvidePlugin } from 'webpack'; 3 | import { createConfig, webpackDir } from './webpack.base.config'; 4 | import { asarDeps } from '../deps' 5 | 6 | const srcDir = path.resolve('src/bootstrap/ext-host'); 7 | const outDir = path.join(webpackDir, 'ext-host'); 8 | 9 | export const extHostConfig = createConfig((_, argv) => ({ 10 | entry: srcDir, 11 | output: { 12 | filename: 'index.js', 13 | path: outDir, 14 | }, 15 | externals: [ 16 | ({ request }, callback) => { 17 | if (asarDeps.includes(request!)) { 18 | return callback(null, 'commonjs ' + request); 19 | } 20 | callback(); 21 | }, 22 | ], 23 | target: 'node', 24 | })) 25 | 26 | export const workerHostConfig = createConfig({ 27 | entry: require.resolve('@opensumi/ide-extension/lib/hosted/worker.host-preload'), 28 | output: { 29 | filename: 'worker-host.js', 30 | path: outDir, 31 | }, 32 | target: 'webworker', 33 | node: { 34 | global: true, 35 | }, 36 | resolve: { 37 | fallback: { 38 | os: false, 39 | util: false, 40 | buffer: require.resolve('buffer/'), 41 | }, 42 | }, 43 | plugins: [ 44 | new ProvidePlugin({ 45 | Buffer: ['buffer', 'Buffer'], 46 | process: 'process/browser', 47 | }), 48 | ], 49 | }) 50 | -------------------------------------------------------------------------------- /build/webpack/webpack.main.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { DefinePlugin } from 'webpack'; 3 | import product from '../../product.json'; 4 | import { createConfig, webpackDir, devServerPort, codeWindowName, updateWindowName } from './webpack.base.config'; 5 | import { asarDeps } from '../deps' 6 | 7 | const srcDir = path.resolve('src/bootstrap/electron-main'); 8 | const outDir = path.resolve(webpackDir, 'main'); 9 | 10 | export default createConfig((_, argv) => ({ 11 | entry: srcDir, 12 | output: { 13 | filename: 'index.js', 14 | path: outDir, 15 | }, 16 | target: 'electron-main', 17 | externals: [ 18 | ({ request }, callback) => { 19 | if (asarDeps.includes(request!)) { 20 | return callback(null, 'commonjs ' + request); 21 | } 22 | callback(); 23 | }, 24 | ], 25 | plugins: [ 26 | new DefinePlugin({ 27 | __PRODUCT__: JSON.stringify(product), 28 | __CODE_WINDOW_NAME__: `'${codeWindowName}'`, 29 | __UPDATE_WINDOW_NAME__: `'${updateWindowName}'`, 30 | __CODE_WINDOW_DEV_SERVER_URL__: argv.mode === 'development' ? `'http://localhost:${devServerPort}/${codeWindowName}'` : "''", 31 | __UPDATE_WINDOW_DEV_SERVER_URL__: argv.mode === 'development' ? `'http://localhost:${devServerPort}/${updateWindowName}'` : "''", 32 | }), 33 | ] 34 | })); 35 | -------------------------------------------------------------------------------- /build/webpack/webpack.node.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { createConfig, webpackDir } from './webpack.base.config'; 3 | import { asarDeps } from '../deps' 4 | 5 | const srcDir = path.resolve('src/bootstrap/node'); 6 | const outDir = path.resolve(webpackDir, 'node'); 7 | 8 | export default createConfig((_, argv) => ({ 9 | entry: srcDir, 10 | output: { 11 | filename: 'index.js', 12 | path: outDir, 13 | }, 14 | target: 'node', 15 | // ws 弱依赖 16 | externals: [ 17 | { 18 | bufferutil: 'commonjs bufferutil', 19 | 'utf-8-validate': 'commonjs utf-8-validate', 20 | }, 21 | ({ request }, callback) => { 22 | if (asarDeps.includes(request!)) { 23 | return callback(null, 'commonjs ' + request); 24 | } 25 | callback(); 26 | }, 27 | ], 28 | })); 29 | -------------------------------------------------------------------------------- /build/webpack/webpack.renderer.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 3 | import CopyPlugin from 'copy-webpack-plugin'; 4 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 5 | import { createConfig, webpackDir, devServerPort, codeWindowName, updateWindowName } from './webpack.base.config'; 6 | 7 | const srcDir = path.resolve('src/bootstrap/browser'); 8 | const outDir = path.resolve(webpackDir, 'renderer'); 9 | const updateSrcDir = path.resolve('src/auto-updater/update-window'); 10 | 11 | export default createConfig((_env, argv) => { 12 | const styleLoader = argv.mode === 'production' ? MiniCssExtractPlugin.loader : 'style-loader' 13 | 14 | return { 15 | entry: { 16 | [codeWindowName]: path.resolve(srcDir, 'index.ts'), 17 | [updateWindowName]: path.resolve(updateSrcDir, 'index.tsx'), 18 | }, 19 | output: { 20 | filename: '[name]/index.js', 21 | path: outDir, 22 | assetModuleFilename: 'assets/[name].[hash][ext]', 23 | }, 24 | devtool: argv.mode === 'production' ? false as const : 'eval-source-map', 25 | target: 'electron-renderer', 26 | externalsPresets: { 27 | node: true, 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.css$/, 33 | use: [styleLoader, 'css-loader'], 34 | }, 35 | { 36 | test: /\.module.less$/, 37 | use: [ 38 | { 39 | loader: styleLoader, 40 | options: { 41 | esModule: false, 42 | } 43 | }, 44 | { 45 | loader: 'css-loader', 46 | options: { 47 | importLoaders: 0, 48 | sourceMap: true, 49 | esModule: false, 50 | modules: { 51 | localIdentName: '[local]___[hash:base64:5]', 52 | }, 53 | }, 54 | }, 55 | { 56 | loader: 'less-loader', 57 | options: { 58 | lessOptions: { 59 | javascriptEnabled: true, 60 | }, 61 | }, 62 | }, 63 | ], 64 | }, 65 | { 66 | test: /^((?!\.module).)*less$/, 67 | use: [ 68 | { 69 | loader: styleLoader, 70 | options: { 71 | esModule: false, 72 | } 73 | }, 74 | { 75 | loader: 'css-loader', 76 | options: { 77 | importLoaders: 0, 78 | esModule: false, 79 | }, 80 | }, 81 | { 82 | loader: 'less-loader', 83 | options: { 84 | lessOptions: { 85 | javascriptEnabled: true, 86 | }, 87 | }, 88 | }, 89 | ], 90 | }, 91 | { 92 | test: /\.(woff(2)?|ttf|eot|svg|png)(\?v=\d+\.\d+\.\d+)?$/, 93 | type: 'asset', 94 | parser: { 95 | dataUrlCondition: { 96 | maxSize: 8 * 1024, 97 | } 98 | } 99 | }, 100 | ], 101 | }, 102 | plugins: [ 103 | new HtmlWebpackPlugin({ 104 | template: path.join(srcDir, 'index.html'), 105 | filename: `${codeWindowName}/index.html`, 106 | chunks: [codeWindowName] 107 | }), 108 | new HtmlWebpackPlugin({ 109 | template: path.join(updateSrcDir, 'index.html'), 110 | filename: `${updateWindowName}/index.html`, 111 | chunks: [updateWindowName] 112 | }), 113 | ...(argv.mode === 'production' ? [ 114 | new MiniCssExtractPlugin({ 115 | filename: '[name]/index.css', 116 | chunkFilename: '[id].css', 117 | }) 118 | ] : []), 119 | new CopyPlugin({ 120 | patterns: [ 121 | { 122 | from: path.resolve(srcDir, 'preload.js'), 123 | to: path.join(outDir, codeWindowName, 'preload.js'), 124 | }, 125 | { 126 | from: require.resolve('@opensumi/ide-monaco/worker/editor.worker.bundle.js'), 127 | to: path.join(outDir, codeWindowName, 'editor.worker.bundle.js'), 128 | }, 129 | { 130 | from: require.resolve('tiktoken/tiktoken_bg.wasm'), 131 | to: path.join(outDir, codeWindowName, 'tiktoken_bg.wasm'), 132 | }, 133 | ], 134 | }), 135 | ], 136 | optimization: { 137 | splitChunks: { 138 | cacheGroups: { 139 | vendor: { 140 | name: 'vendor', 141 | chunks: 'all', 142 | minChunks: 2, 143 | }, 144 | }, 145 | } 146 | }, 147 | infrastructureLogging: { 148 | level: 'none' 149 | }, 150 | stats: 'none', 151 | devServer: { 152 | hot: true, 153 | devMiddleware: { 154 | writeToDisk: true, 155 | }, 156 | client: { 157 | overlay: { 158 | runtimeErrors: false, 159 | warnings: false, 160 | } 161 | }, 162 | historyApiFallback: true, 163 | port: devServerPort, 164 | setupExitSignals: true, 165 | static: outDir, 166 | headers: { 167 | 'Content-Security-Policy': "default-src 'self' 'unsafe-inline' data: file:; script-src 'self' 'unsafe-eval' 'unsafe-inline' data: file:; connect-src 'self' file:; worker-src 'self' data: blob:; img-src 'self' data: file:", 168 | }, 169 | } 170 | } 171 | }); 172 | -------------------------------------------------------------------------------- /build/webpack/webpack.watcher-host.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { createConfig, webpackDir } from './webpack.base.config'; 3 | import { asarDeps } from '../deps'; 4 | 5 | const srcDir = path.resolve('src/bootstrap/watcher-host'); 6 | const outDir = path.join(webpackDir, 'watcher-host'); 7 | 8 | export const watcherHostConfig = createConfig(() => ({ 9 | entry: srcDir, 10 | output: { 11 | filename: 'index.js', 12 | path: outDir, 13 | }, 14 | externals: [ 15 | ({ request }, callback) => { 16 | if (asarDeps.includes(request!)) { 17 | return callback(null, 'commonjs ' + request); 18 | } 19 | callback(); 20 | }, 21 | ], 22 | target: 'node', 23 | })); -------------------------------------------------------------------------------- /build/webpack/webpack.webview.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import CopyPlugin from 'copy-webpack-plugin'; 3 | import { createConfig, webpackDir } from './webpack.base.config'; 4 | 5 | const outDir = path.join(webpackDir, 'webview'); 6 | 7 | export default createConfig( 8 | { 9 | entry: require.resolve('@opensumi/ide-webview/lib/electron-webview/host-preload.js'), 10 | output: { 11 | filename: 'host-preload.js', 12 | path: outDir, 13 | }, 14 | target: 'electron-preload', 15 | plugins: [ 16 | new CopyPlugin({ 17 | patterns: [ 18 | { 19 | from: require.resolve('@opensumi/ide-webview/lib/electron-webview/plain-preload.js'), 20 | to: path.join(outDir, 'plain-preload.js'), 21 | }, 22 | ], 23 | }), 24 | ], 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /forge.config.ts: -------------------------------------------------------------------------------- 1 | import type { ForgeConfig } from '@electron-forge/shared-types'; 2 | import path from 'path' 3 | import fsp from 'fs/promises' 4 | import { downloadExtensions } from './build/download-extensions' 5 | import { WebpackPlugin } from './build/webpack/ForgeWebpackPlugin' 6 | import { buildAsar } from './build/build-asar' 7 | import { signWinApp } from './build/util' 8 | import { rebuild } from './build/rebuild' 9 | import { MakerNsis } from './build/maker-nisi' 10 | import { MakerZip } from './build/maker-zip' 11 | import packageData from './package.json' 12 | import productData from './product.json' 13 | 14 | const config: ForgeConfig = { 15 | packagerConfig: { 16 | asar: false, 17 | appVersion: packageData.version, 18 | name: productData.productName, 19 | appBundleId: productData.darwinBundleIdentifier, 20 | icon: path.join(__dirname, './assets/icon/icon'), 21 | extendInfo: { 22 | CFBundleIconFile: 'icon.icns', 23 | }, 24 | ignore: (file: string) => { 25 | if (!file) return false; 26 | return !/^[/\\](out|extensions|package\.json|product\.json)($|[/\\]).*$/.test(file); 27 | }, 28 | prune: false, 29 | afterFinalizePackageTargets: [ 30 | async (targets, done) => { 31 | for (const target of targets) { 32 | await rebuild({ arch: target.arch }) 33 | await buildAsar(path.resolve(`out/asar/${target.arch}`)) 34 | } 35 | done() 36 | } 37 | ], 38 | afterCopy: [ 39 | async (appPath, electronVersion, pPlatform, pArch, done) => { 40 | const asarDir = path.join(appPath, 'out/asar') 41 | const files = await fsp.readdir(path.join(asarDir, pArch)) 42 | for (const filename of files) { 43 | await fsp.rename(path.join(asarDir, pArch, filename), path.join(appPath, filename)) 44 | } 45 | await fsp.rm(asarDir, { recursive: true }) 46 | done() 47 | } 48 | ], 49 | afterComplete: [ 50 | async (finalPath, electronVersion, pPlatform, pArch, done) => { 51 | if (process.env.MAC_NOTARIZE_SCRIPT) { 52 | const mod = await import(process.env.MAC_NOTARIZE_SCRIPT) 53 | const fn = typeof mod === 'object' && mod && mod.default ? mod.default : mod; 54 | if (typeof fn === 'function') { 55 | await fn(path.join(finalPath, `${productData.productName}.app`), productData.darwinBundleIdentifier) 56 | } 57 | } 58 | done() 59 | } 60 | ], 61 | extraResource: [ 62 | path.join(__dirname, './assets/app-update.yml'), // for electron-updater 63 | ], 64 | ...(process.env.WINDOWS_SIGN_TOOL_PATH ? ({ 65 | windowsSign: { 66 | hookFunction: file => signWinApp(file) 67 | }, 68 | }) : null), 69 | ...(process.env.OSX_SIGN_IDENTITY ? { 70 | osxSign: { 71 | identity: process.env.OSX_SIGN_IDENTITY, 72 | }, 73 | } : null), 74 | }, 75 | outDir: 'dist', 76 | // @electron/rebuild 不支持 node-gyp 10,手动构建,跳过 forge 的自动构建 77 | rebuildConfig: { 78 | onlyModules: [], 79 | }, 80 | makers: [new MakerNsis(), new MakerZip()], 81 | plugins: [ 82 | new WebpackPlugin({}), 83 | ], 84 | hooks: { 85 | generateAssets: async () => { 86 | await downloadExtensions(); 87 | }, 88 | }, 89 | }; 90 | 91 | export default config; 92 | -------------------------------------------------------------------------------- /product.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "CodeFuse IDE", 3 | "applicationName": "CodeFuseIDE", 4 | "dataFolderName": ".codefuse-ide", 5 | "urlProtocol": "codefuse-ide", 6 | "darwinBundleIdentifier": "com.alipay.codefuse.ide", 7 | "autoUpdaterConfigUrl": "https://render.alipay.com/p/yuyan/codefuse-ide-data_auto-update/zh_CN.json", 8 | "commit": "", 9 | "date": "" 10 | } 11 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CodeFuse IDE 6 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ai/browser/ai-run.contribution.ts: -------------------------------------------------------------------------------- 1 | import { Autowired } from '@opensumi/di'; 2 | import { ClientAppContribution, Domain, MaybePromise, ProgressLocation } from '@opensumi/ide-core-browser'; 3 | import { DebugConfigurationManager } from '@opensumi/ide-debug/lib/browser/debug-configuration-manager'; 4 | import { IProgressService } from '@opensumi/ide-core-browser/lib/progress'; 5 | import { MessageService } from '@opensumi/ide-overlay/lib/browser/message.service'; 6 | import { AiRunService } from './ai-run.service'; 7 | 8 | @Domain(ClientAppContribution) 9 | export class AIRunContribution implements ClientAppContribution { 10 | 11 | @Autowired(DebugConfigurationManager) 12 | private readonly debugConfigurationManager: DebugConfigurationManager; 13 | 14 | @Autowired(AiRunService) 15 | private readonly aiRunService: AiRunService; 16 | 17 | @Autowired(IProgressService) 18 | private readonly progressService: IProgressService; 19 | 20 | @Autowired(MessageService) 21 | protected readonly messageService: MessageService; 22 | 23 | onDidStart(): MaybePromise { 24 | this.registerDebugConfiguration(); 25 | } 26 | 27 | async registerDebugConfiguration() { 28 | this.debugConfigurationManager.registerInternalDebugConfigurationProvider('ai-native', { 29 | type: 'ai-native', 30 | label: 'AI 生成配置', 31 | provideDebugConfigurations: async () => { 32 | const aiConfig = await this.progressService.withProgress( 33 | { 34 | location: ProgressLocation.Notification, 35 | title: 'AI 配置生成中', 36 | }, 37 | async () => { 38 | return this.aiRunService.getNodejsDebugConfigurations(); 39 | }, 40 | ); 41 | 42 | if (!aiConfig || aiConfig.length === 0) { 43 | this.messageService.info('AI 配置生成失败'); 44 | } else { 45 | this.messageService.info('AI 配置生成成功'); 46 | } 47 | 48 | return aiConfig || []; 49 | }, 50 | }); 51 | this.debugConfigurationManager.registerInternalDebugConfigurationOverride('pwa-node', { 52 | type: 'pwa-node', 53 | label: 'Node.js 项目自动生成', 54 | popupHint: '通过 Node.js Debug 提供的服务自动分析项目,生成运行配置', 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /src/ai/browser/assets/hi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codefuse-ai/codefuse-ide/2f4cfb25c8f8a04f540287aa38f72e22813cee7c/src/ai/browser/assets/hi.png -------------------------------------------------------------------------------- /src/ai/browser/command/command-render.module.less: -------------------------------------------------------------------------------- 1 | .chat_excute_result { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | padding-bottom: 4px; 6 | .chat_result_list { 7 | margin: 8px 0 12px; 8 | } 9 | 10 | .chat_excute_btn { 11 | border-radius: 8px; 12 | } 13 | } -------------------------------------------------------------------------------- /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/components/left-toolbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AIRunToolbar } from '@opensumi/ide-ai-native/lib/browser/contrib/run-toolbar/run-toolbar'; 3 | 4 | export const LeftToolbar = () =>{ 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /src/ai/browser/constants.ts: -------------------------------------------------------------------------------- 1 | export const AI_MENU_BAR_LEFT_ACTION = 'ai-menu-bar-left-action'; 2 | // export const AI_MENU_BAR_RIGHT_ACTION = 'ai-menu-bar-right-action'; 3 | 4 | export enum EInlineOperation { 5 | Explain = 'Explain', 6 | Comments = 'Comments', 7 | Test = 'Test', 8 | Optimize = 'Optimize', 9 | } 10 | -------------------------------------------------------------------------------- /src/ai/browser/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@opensumi/ide-core-browser'; 2 | import { Injectable } from '@opensumi/di'; 3 | 4 | import { AINativeContribution } from './ai-native.contribution' 5 | import { AIRunContribution } from './ai-run.contribution' 6 | import { AICommandPromptManager } from './command/command-prompt-manager' 7 | import { AICommandService } from './command/command.service' 8 | import { InlineChatOperationModel } from './inline-chat-operation' 9 | import { AIModelContribution } from './ai-model.contribution' 10 | import { AIModelServicePath } from '../common' 11 | 12 | export * from './constants' 13 | 14 | @Injectable() 15 | export class AIFeatureModule extends BrowserModule { 16 | providers = [ 17 | AINativeContribution, 18 | AIRunContribution, 19 | AICommandPromptManager, 20 | AICommandService, 21 | InlineChatOperationModel, 22 | AIModelContribution, 23 | ]; 24 | 25 | backServices = [ 26 | { 27 | servicePath: AIModelServicePath, 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/ai/browser/inline-chat-operation.ts: -------------------------------------------------------------------------------- 1 | import { Autowired, Injectable } from "@opensumi/di"; 2 | import { ChatService } from "@opensumi/ide-ai-native/lib/browser/chat/chat.api.service"; 3 | import { InlineChatController } from "@opensumi/ide-ai-native/lib/browser/widget/inline-chat/inline-chat-controller"; 4 | import { AIBackSerivcePath, CancellationToken, ChatServiceToken, IAIBackService } from "@opensumi/ide-core-common"; 5 | import { ICodeEditor } from "@opensumi/ide-monaco"; 6 | import { commentsPrompt, explainPrompt, optimizePrompt, testPrompt } from "./prompt"; 7 | import { EInlineOperation } from './constants'; 8 | 9 | @Injectable() 10 | export class InlineChatOperationModel { 11 | @Autowired(AIBackSerivcePath) 12 | private readonly aiBackService: IAIBackService; 13 | 14 | @Autowired(ChatServiceToken) 15 | private readonly aiChatService: ChatService; 16 | 17 | private getCrossCode(monacoEditor: ICodeEditor): string { 18 | const model = monacoEditor.getModel(); 19 | if (!model) { 20 | return ''; 21 | } 22 | 23 | const selection = monacoEditor.getSelection(); 24 | 25 | if (!selection) { 26 | return ''; 27 | } 28 | 29 | const crossSelection = selection 30 | .setStartPosition(selection.startLineNumber, 1) 31 | .setEndPosition(selection.endLineNumber, Number.MAX_SAFE_INTEGER); 32 | const crossCode = model.getValueInRange(crossSelection); 33 | return crossCode; 34 | } 35 | 36 | public [EInlineOperation.Explain](monacoEditor: ICodeEditor): void { 37 | const model = monacoEditor.getModel(); 38 | if (!model) { 39 | return; 40 | } 41 | 42 | const crossCode = this.getCrossCode(monacoEditor); 43 | 44 | this.aiChatService.sendMessage({ 45 | message: `解释以下代码: \n\`\`\`${model.getLanguageId()}\n${crossCode}\n\`\`\``, 46 | prompt: explainPrompt(model.getLanguageId(), crossCode), 47 | }); 48 | } 49 | 50 | public async [EInlineOperation.Comments](editor: ICodeEditor, token: CancellationToken): Promise { 51 | const crossCode = this.getCrossCode(editor); 52 | const prompt = commentsPrompt(crossCode); 53 | 54 | const controller = new InlineChatController({ enableCodeblockRender: true }); 55 | const stream = await this.aiBackService.requestStream(prompt, { noTool: true }, token); 56 | controller.mountReadable(stream); 57 | 58 | return controller; 59 | } 60 | 61 | public [EInlineOperation.Test](editor: ICodeEditor): void { 62 | const model = editor.getModel(); 63 | if (!model) { 64 | return; 65 | } 66 | 67 | const crossCode = this.getCrossCode(editor); 68 | const prompt = testPrompt(crossCode); 69 | 70 | this.aiChatService.sendMessage({ 71 | message: `为以下代码写单测:\n\`\`\`${model.getLanguageId()}\n${crossCode}\n\`\`\``, 72 | prompt, 73 | }); 74 | } 75 | 76 | public async [EInlineOperation.Optimize](editor: ICodeEditor, token: CancellationToken): Promise { 77 | const crossCode = this.getCrossCode(editor); 78 | const prompt = optimizePrompt(crossCode); 79 | 80 | const controller = new InlineChatController({ enableCodeblockRender: true }); 81 | const stream = await this.aiBackService.requestStream(prompt, { noTool: true }, token); 82 | controller.mountReadable(stream); 83 | 84 | return controller; 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /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/ai/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' -------------------------------------------------------------------------------- /src/ai/common/types.ts: -------------------------------------------------------------------------------- 1 | export const AIModelServicePath = 'AIModelServicePath'; 2 | 3 | export const IAIModelServiceProxy = Symbol('IAIModelServiceProxy') 4 | 5 | export interface IAIModelServiceProxy { 6 | setConfig(values: Record): Promise 7 | } 8 | 9 | export const ModelSettingId = { 10 | baseUrl: 'ai.model.baseUrl', 11 | apiKey: 'ai.model.apiKey', 12 | codeModelName: 'ai.model.code.modelName', 13 | codeSystemPrompt: 'ai.model.code.systemPrompt', 14 | codeFimTemplate: 'ai.model.code.fimTemplate', 15 | codeTemperature: 'ai.model.code.temperature', 16 | codeMaxTokens: 'ai.model.code.maxTokens', 17 | codePresencePenalty: 'ai.model.code.presencePenalty', 18 | codeFrequencyPenalty: 'ai.model.code.frequencyPenalty', 19 | codeTopP: 'ai.model.code.topP', 20 | } 21 | 22 | export type IModelConfig = Record; 23 | -------------------------------------------------------------------------------- /src/ai/node/index.ts: -------------------------------------------------------------------------------- 1 | import { NodeModule } from '@opensumi/ide-core-node'; 2 | import { Injectable, Provider } from '@opensumi/di'; 3 | import { AIBackSerivceToken } from '@opensumi/ide-core-common/lib/types/ai-native'; 4 | import { IShellIntegrationService } from '@opensumi/ide-terminal-next/lib/node/shell-integration.service'; 5 | 6 | import { ShellIntegrationService } from './shell-integration' 7 | import { AIBackService } from './ai-back.service' 8 | import { AIModelServiceProxy, AIModelService } from './model.service' 9 | import { AIModelServicePath, IAIModelServiceProxy } from '../common' 10 | 11 | @Injectable() 12 | export class AIServiceModule extends NodeModule { 13 | providers: Provider[] = [ 14 | { 15 | token: AIBackSerivceToken, 16 | useClass: AIBackService, 17 | override: true, 18 | }, 19 | { 20 | token: IShellIntegrationService, 21 | useClass: ShellIntegrationService, 22 | override: true, 23 | }, 24 | { 25 | token: AIModelService, 26 | useClass: AIModelService, 27 | }, 28 | { 29 | token: IAIModelServiceProxy, 30 | useClass: AIModelServiceProxy, 31 | } 32 | ] 33 | 34 | backServices = [ 35 | { 36 | servicePath: AIModelServicePath, 37 | token: IAIModelServiceProxy, 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/ai/node/model.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Autowired } from '@opensumi/di'; 2 | import { INodeLogger } from '@opensumi/ide-core-node' 3 | import { IAIModelServiceProxy, IModelConfig } from '../common' 4 | import { ILogServiceManager } from '@opensumi/ide-logs'; 5 | 6 | @Injectable() 7 | export class AIModelService { 8 | private logger: INodeLogger 9 | 10 | @Autowired(ILogServiceManager) 11 | private readonly loggerManager: ILogServiceManager; 12 | 13 | #config: IModelConfig | undefined 14 | 15 | constructor() { 16 | this.logger = this.loggerManager.getLogger('ai' as any); 17 | } 18 | 19 | get config(): IModelConfig | undefined { 20 | const config = this.#config 21 | if (!config) return 22 | return { 23 | ...config, 24 | codeTemperature: this.coerceNumber(config.codeTemperature, 0, 1, 0.2), 25 | codePresencePenalty: this.coerceNumber(config.codePresencePenalty, -2, 2, 1), 26 | codeFrequencyPenalty: this.coerceNumber(config.codeFrequencyPenalty, -2, 2, 1), 27 | codeTopP: this.coerceNumber(config.codeTopP, 0, 1, 0.95), 28 | } 29 | } 30 | 31 | async setConfig(config: IModelConfig): Promise { 32 | this.#config = config; 33 | this.logger.log('[model config]', JSON.stringify(config)); 34 | } 35 | 36 | private coerceNumber(value: string | number, min: number, max: number, defaultValue: number) { 37 | const num = Number(value) 38 | if (isNaN(num)) return defaultValue 39 | if (num < min || num > max) return defaultValue 40 | return num 41 | } 42 | } 43 | 44 | @Injectable() 45 | export class AIModelServiceProxy implements IAIModelServiceProxy { 46 | @Autowired(AIModelService) 47 | private readonly modelService: AIModelService; 48 | 49 | async setConfig(config: IModelConfig): Promise { 50 | this.modelService.setConfig(config) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ai/node/pty/pty.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@opensumi/di'; 2 | import { ITerminalLaunchError } from '@opensumi/ide-terminal-next'; 3 | import { PtyService } from '@opensumi/ide-terminal-next/lib/node/pty'; 4 | import { getShellPath } from '@opensumi/ide-core-node/lib/bootstrap/shell-path'; 5 | 6 | import { bashIntegrationPath, initShellIntegrationFile } from './shell-integration'; 7 | 8 | @Injectable({ multiple: true }) 9 | export class AIPtyService extends PtyService { 10 | async start(): Promise { 11 | const { shellLaunchConfig } = this; 12 | 13 | let ptyEnv: { [key: string]: string | undefined } | undefined; 14 | if (shellLaunchConfig.strictEnv) { 15 | ptyEnv = shellLaunchConfig.env as { [key: string]: string | undefined }; 16 | } else { 17 | ptyEnv = { 18 | ...process.env, 19 | PATH: await getShellPath(), 20 | LC_ALL: `zh_CN.UTF-8`, 21 | LANG: `zh_CN.UTF-8`, 22 | ...shellLaunchConfig.env, 23 | }; 24 | } 25 | 26 | if (shellLaunchConfig.executable?.includes('bash')) { 27 | await initShellIntegrationFile(); 28 | if (!shellLaunchConfig.args) { shellLaunchConfig.args = []; } 29 | if (Array.isArray(shellLaunchConfig.args)) { 30 | shellLaunchConfig.args.push('--init-file', bashIntegrationPath); 31 | } 32 | } 33 | 34 | this._ptyOptions['env'] = ptyEnv; 35 | 36 | try { 37 | await this.setupPtyProcess(); 38 | return undefined; 39 | } catch (err: any) { 40 | this.logger.error('IPty#spawn native exception', err); 41 | return { message: `A native exception occurred during launch (${err.message})` }; 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/ai/node/pty/shell-integration.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises'; 2 | import * as path from 'node:path'; 3 | import * as os from 'node:os'; 4 | 5 | const shellIntegrationDirPath = path.join(os.homedir(), process.env.IDE_DATA_FOLDER_NAME!, 'shell-integration'); 6 | 7 | export const bashIntegrationPath = path.join(shellIntegrationDirPath, 'bash-integration.bash'); 8 | 9 | /** 10 | 注入的 bash initfile,用于 ShellIntergration 功能的搭建 11 | 后续会针对 ShellIntergation 做整体的架构设计,目前满足基础功能需求 12 | */ 13 | export const bashIntegrationContent = String.raw` 14 | 15 | if [ -r /etc/profile ]; then 16 | . /etc/profile 17 | fi 18 | if [ -r ~/.bashrc ]; then 19 | . ~/.bashrc 20 | fi 21 | if [ -r ~/.bash_profile ]; then 22 | . ~/.bash_profile 23 | elif [ -r ~/.bash_login ]; then 24 | . ~/.bash_login 25 | elif [ -r ~/.profile ]; then 26 | . ~/.profile 27 | fi 28 | 29 | __is_prompt_start() { 30 | builtin printf '\e]6973;PS\a' 31 | } 32 | 33 | __is_prompt_end() { 34 | builtin printf '\e]6973;PE\a' 35 | } 36 | 37 | __is_update_prompt() { 38 | if [[ "$__is_custom_PS1" == "" || "$__is_custom_PS1" != "$PS1" ]]; then 39 | __is_original_PS1=$PS1 40 | __is_custom_PS1="\[$(__is_prompt_start)\]$__is_original_PS1\[$(__is_prompt_end)\]" 41 | export PS1="$__is_custom_PS1" 42 | fi 43 | } 44 | 45 | __is_update_prompt 46 | `; 47 | 48 | export const initShellIntegrationFile = async () => { 49 | await fs.mkdir(shellIntegrationDirPath, { recursive: true }); 50 | await fs.writeFile(bashIntegrationPath, bashIntegrationContent); 51 | }; 52 | -------------------------------------------------------------------------------- /src/ai/node/shell-integration.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises'; 2 | import * as path from 'node:path'; 3 | import * as os from 'node:os'; 4 | import { Autowired, Injectable } from '@opensumi/di' 5 | import { ShellIntegrationService as BaseShellIntegrationService } from '@opensumi/ide-terminal-next/lib/node/shell-integration.service' 6 | 7 | export class ShellIntegrationService extends BaseShellIntegrationService { 8 | @Autowired(ShellIntegrationService) 9 | shellIntegrationService: ShellIntegrationService 10 | 11 | async initBashInitFile(): Promise { 12 | const shellIntegrationDirPath = path.join(os.homedir(), process.env.IDE_DATA_FOLDER_NAME!, 'shell-integration'); 13 | const bashIntegrationPath = path.join(shellIntegrationDirPath, 'bash-integration.bash'); 14 | await fs.mkdir(shellIntegrationDirPath, { recursive: true }); 15 | await fs.writeFile(bashIntegrationPath, await this.getBashIntegrationContent()); 16 | return bashIntegrationPath; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ai/node/types.ts: -------------------------------------------------------------------------------- 1 | interface ToolCall { 2 | id: string; 3 | type: string; 4 | function: { 5 | name: string; 6 | arguments: string; 7 | } 8 | } 9 | 10 | interface Message { 11 | role: string; 12 | content: any; 13 | tool_calls: ToolCall[]; 14 | } 15 | 16 | export interface Choice { 17 | index: number; 18 | message: Message; 19 | finish_reason?: string; 20 | } 21 | 22 | export interface ChunkChoice { 23 | index: number; 24 | delta: Message; 25 | finish_reason?: string; 26 | } 27 | 28 | interface Usage { 29 | prompt_tokens: number; 30 | completion_tokens: number; 31 | total_tokens: number; 32 | } 33 | 34 | export interface ChatCompletion { 35 | id: string; 36 | object: string; 37 | created: string; 38 | model: string; 39 | system_fingerprint: string; 40 | choices: Choice[]; 41 | usage?: Usage; 42 | } 43 | 44 | export interface ChatCompletionChunk { 45 | id: string; 46 | object: string; 47 | created: string; 48 | model: string; 49 | system_fingerprint: string; 50 | choices: ChunkChoice[]; 51 | } 52 | 53 | export interface CompletionChoice { 54 | finish_reason: string; 55 | index: number; 56 | text: string; 57 | } 58 | 59 | export interface Completion { 60 | id: string; 61 | choices: Array; 62 | created: number; 63 | model: string; 64 | object: 'text_completion'; 65 | system_fingerprint?: string; 66 | usage?: Usage; 67 | } 68 | -------------------------------------------------------------------------------- /src/auto-updater/browser/index.ts: -------------------------------------------------------------------------------- 1 | import { Provider, Injectable } from '@opensumi/di'; 2 | import { BrowserModule, createElectronMainApi } from '@opensumi/ide-core-browser'; 3 | import { UpdaterContribution } from './update.contribution' 4 | import { IUpdateMainService } from '../common' 5 | 6 | @Injectable() 7 | export class AutoUpdaterModule extends BrowserModule { 8 | providers: Provider[] = [ 9 | UpdaterContribution, 10 | { 11 | token: IUpdateMainService, 12 | useValue: createElectronMainApi(IUpdateMainService) 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/auto-updater/browser/update.contribution.ts: -------------------------------------------------------------------------------- 1 | import { Autowired } from '@opensumi/di' 2 | import { Command, CommandRegistry, Domain, localize } from '@opensumi/ide-core-common' 3 | import { BrowserModule, CommandContribution } from '@opensumi/ide-core-browser'; 4 | import { MenuId, MenuContribution, IMenuRegistry } from '@opensumi/ide-core-browser/lib/menu/next'; 5 | import { IUpdateMainService } from '../common' 6 | 7 | const CHECK_COMMAND_ID = { 8 | id: 'autoUpdater.checkForUpdates', 9 | label: localize('autoUpdater.checkForUpdates'), 10 | } 11 | 12 | @Domain(MenuContribution, CommandContribution) 13 | export class UpdaterContribution implements MenuContribution, CommandContribution { 14 | @Autowired(IUpdateMainService) 15 | updateService: IUpdateMainService 16 | 17 | registerCommands(commands: CommandRegistry): void { 18 | commands.registerCommand( 19 | { id: CHECK_COMMAND_ID.id }, 20 | { 21 | execute: async () => { 22 | await this.updateService.checkForUpdatesManual() 23 | } 24 | } 25 | ) 26 | } 27 | 28 | registerMenus(menuRegistry: IMenuRegistry) { 29 | menuRegistry.registerMenuItem(MenuId.MenubarAppMenu, { 30 | group: '0_about', 31 | order: 1, 32 | command: { 33 | id: CHECK_COMMAND_ID.id, 34 | label: CHECK_COMMAND_ID.label, 35 | }, 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/auto-updater/common/index.ts: -------------------------------------------------------------------------------- 1 | import type { UpdateInfo, ProgressInfo } from 'electron-updater' 2 | 3 | export { UpdateInfo, ProgressInfo } 4 | 5 | export const enum UpdateState { 6 | NoAvailable = 'NoAvailable', 7 | Checking = 'Checking', 8 | CheckingError = 'CheckingError', 9 | Available = 'Available', 10 | Downloading = 'Downloading', 11 | DownloadError = 'DownloadError', 12 | Downloaded = 'Downloaded', 13 | UpdateError = 'UpdateError' 14 | } 15 | 16 | export const IUpdateMainService = 'IUpdateMainService' 17 | export interface IUpdateMainService { 18 | checkForUpdatesManual(): Promise 19 | } 20 | 21 | export enum IPC_CHANNEL { 22 | initialState = 'initialState', 23 | downloadAndInstall = 'downloadAndInstall', 24 | eventData = 'eventData', 25 | ignoreVersion = 'ignoreVersion' 26 | } 27 | 28 | export interface InitialState { 29 | updateState: UpdateState, 30 | updateInfo: UpdateInfo | null, 31 | progressInfo: ProgressInfo | null, 32 | } 33 | 34 | export interface EventData { 35 | event: string; 36 | data?: any; 37 | } 38 | -------------------------------------------------------------------------------- /src/auto-updater/electron-main/index.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Provider } from '@opensumi/di'; 2 | import { ElectronMainModule } from '@opensumi/ide-core-electron-main/lib/electron-main-module'; 3 | import { UpdateContribution } from './update.contribution' 4 | import { UpdateMainService } from './update.service' 5 | import { UpdateWindow } from './update-window'; 6 | import { AutoUpdaterService } from './auto-updater.service' 7 | 8 | 9 | @Injectable() 10 | export class AutoUpdaterModule extends ElectronMainModule { 11 | providers: Provider[] = [ 12 | UpdateContribution, 13 | UpdateMainService, 14 | UpdateWindow, 15 | AutoUpdaterService, 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /src/auto-updater/electron-main/update-window.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | import { Autowired, Injectable } from '@opensumi/di' 3 | import { Disposable } from '@opensumi/ide-core-common' 4 | import path from 'node:path' 5 | import { AutoUpdaterService } from './auto-updater.service' 6 | import { IPC_CHANNEL } from '../common' 7 | 8 | @Injectable() 9 | export class UpdateWindow { 10 | #browserWindow: BrowserWindow | null = null 11 | private get browserWindow() { 12 | let win = this.#browserWindow 13 | if (!win || win.isDestroyed()) { 14 | win = this.createWindow() 15 | this.#browserWindow = win 16 | } 17 | return win; 18 | } 19 | 20 | @Autowired(AutoUpdaterService) 21 | autoUpdaterService: AutoUpdaterService 22 | 23 | openWindow() { 24 | const { browserWindow } = this 25 | browserWindow.show() 26 | } 27 | 28 | private createWindow() { 29 | const disposable = new Disposable() 30 | const win = new BrowserWindow({ 31 | width: 620, 32 | height: 400, 33 | minWidth: 0, 34 | maxWidth: 0, 35 | resizable: false, 36 | fullscreenable: false, 37 | title: 'CodeFuse IDE Update', 38 | backgroundColor: '#ECECEC', 39 | webPreferences: { 40 | nodeIntegration: true, 41 | contextIsolation: false, 42 | webSecurity: true, 43 | }, 44 | }) 45 | if (__UPDATE_WINDOW_DEV_SERVER_URL__) { 46 | win.loadURL(__UPDATE_WINDOW_DEV_SERVER_URL__) 47 | } else { 48 | win.loadFile(path.join(__dirname, `../renderer/${__UPDATE_WINDOW_NAME__}/index.html`)) 49 | } 50 | win.on('closed', () => { 51 | this.#browserWindow = null 52 | disposable.dispose() 53 | }) 54 | const { webContents } = win 55 | const { ipc } = webContents 56 | ipc.handle(IPC_CHANNEL.initialState, () => { 57 | return { 58 | updateState: this.autoUpdaterService.updateState, 59 | updateInfo: this.autoUpdaterService.updateInfo, 60 | progressInfo: this.autoUpdaterService.progressInfo, 61 | } 62 | }) 63 | ipc.handle(IPC_CHANNEL.downloadAndInstall, async () => { 64 | try { 65 | await this.autoUpdaterService.downloadUpdate() 66 | } catch { 67 | throw new Error('download error') 68 | } 69 | }) 70 | ipc.on(IPC_CHANNEL.ignoreVersion, () => { 71 | this.autoUpdaterService.updateIgnoreVersion() 72 | this.#browserWindow?.close() 73 | this.#browserWindow = null 74 | }) 75 | disposable.addDispose( 76 | this.autoUpdaterService.updateEvent((data) => { 77 | webContents.send(IPC_CHANNEL.eventData, data) 78 | }) 79 | ) 80 | 81 | return win; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/auto-updater/electron-main/update.contribution.ts: -------------------------------------------------------------------------------- 1 | import { Autowired } from '@opensumi/di' 2 | import { Domain, MaybePromise } from '@opensumi/ide-core-common' 3 | import { ElectronMainApiRegistry } from '@opensumi/ide-core-electron-main' 4 | import { ElectronMainContribution} from '@/core/electron-main' 5 | import { UpdateMainService } from './update.service' 6 | import { UpdateWindow } from './update-window' 7 | import { IUpdateMainService } from '../common' 8 | 9 | @Domain(ElectronMainContribution) 10 | export class UpdateContribution implements ElectronMainContribution { 11 | @Autowired(UpdateMainService) 12 | updateMainService: UpdateMainService 13 | 14 | @Autowired(UpdateWindow) 15 | updateWindow: UpdateWindow 16 | 17 | onStart(): MaybePromise { 18 | this.updateMainService.checkForUpdatesAuto() 19 | } 20 | 21 | registerMainApi(registry: ElectronMainApiRegistry): void { 22 | registry.registerMainApi(IUpdateMainService, this.updateMainService); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/auto-updater/electron-main/update.provider.ts: -------------------------------------------------------------------------------- 1 | import { Provider, ResolvedUpdateFileInfo, UpdateInfo, AppUpdater } from 'electron-updater' 2 | import yaml from 'js-yaml' 3 | import type { CustomPublishOptions as BaseCustomPublishOptions } from 'builder-util-runtime' 4 | 5 | export interface CustomPublishOptions extends BaseCustomPublishOptions { 6 | readonly configUrl: string 7 | } 8 | 9 | interface AutoUpdateConfig { 10 | platform: string; 11 | channel: string; 12 | channelUrl: string; 13 | releaseNote: string; 14 | stagingPercentage: number; 15 | } 16 | 17 | const newError = (message: string, code: string) => { 18 | const error = new Error(message) 19 | ;(error as NodeJS.ErrnoException).code = code; 20 | return error 21 | } 22 | 23 | export class CustomProvider extends Provider { 24 | constructor( 25 | private readonly configuration: CustomPublishOptions, 26 | private readonly updater: AppUpdater, 27 | runtimeOptions: any, 28 | ) { 29 | super(runtimeOptions) 30 | } 31 | 32 | private get channel(): string { 33 | const result = this.updater.channel || this.configuration.channel 34 | return result == null ? this.getDefaultChannelName() : this.getCustomChannelName(result) 35 | } 36 | 37 | async getLatestVersion(): Promise { 38 | const channelFile = `${this.channel}.yml` 39 | for (let attemptNumber = 0; ; attemptNumber++) { 40 | try { 41 | const rawData = await this.httpRequest(new URL(this.configuration.configUrl)) 42 | if (!rawData) { 43 | throw newError(`Cannot get config data in the latest release config (${this.configuration.configUrl}): rawData: null`, 'ERR_UPDATER_INVALID_UPDATE_INFO') 44 | } 45 | let config: AutoUpdateConfig[] 46 | try { 47 | config = JSON.parse(rawData) 48 | } catch (err: any) { 49 | throw newError( 50 | `Cannot parse update info in the latest release config (${this.configuration.configUrl}): ${err.stack || err.message}, rawData: ${rawData}`, 51 | 'ERR_UPDATER_INVALID_UPDATE_INFO' 52 | ) 53 | } 54 | const channelConfig = config.find(item => item.channel === this.channel) 55 | if (!channelConfig) { 56 | throw newError( 57 | `Cannot find chanel update info in the latest release config (${this.configuration.configUrl}), rawData: ${rawData}`, 58 | 'ERR_UPDATER_INVALID_UPDATE_INFO' 59 | ) 60 | } 61 | const { channelUrl, stagingPercentage, releaseNote } = channelConfig; 62 | const channelRawData = await this.httpRequest(new URL(channelUrl)) 63 | if (!channelRawData) { 64 | throw newError(`Cannot get channel data in latest release channel (${channelUrl}): rawData: null`, 'ERR_UPDATER_INVALID_UPDATE_INFO') 65 | } 66 | let updateInfo: UpdateInfo 67 | try { 68 | updateInfo = yaml.load(channelRawData) as UpdateInfo 69 | Object.assign(updateInfo, { 70 | stagingPercentage, 71 | releaseNotes: releaseNote, 72 | }) 73 | } catch (err) { 74 | throw newError(`Cannot prase update info in latest release channel (${channelUrl}): rawData: ${channelRawData}`, 'ERR_UPDATER_INVALID_UPDATE_INFO') 75 | } 76 | return updateInfo 77 | } catch (e: any) { 78 | if ('statusCode' in e && e.statusCode === 404) { 79 | throw newError(`Cannot find channel "${channelFile}" update info: ${e.stack || e.message}`, "ERR_UPDATER_CHANNEL_FILE_NOT_FOUND") 80 | } else if (e.code === "ECONNREFUSED") { 81 | if (attemptNumber < 3) { 82 | await new Promise((resolve, reject) => { 83 | try { 84 | setTimeout(resolve, 1000 * attemptNumber) 85 | } catch (e: any) { 86 | reject(e) 87 | } 88 | }) 89 | continue 90 | } 91 | } 92 | throw e 93 | } 94 | } 95 | } 96 | 97 | resolveFiles(updateInfo: UpdateInfo): Array { 98 | return updateInfo.files.map(info => ({ 99 | url: new URL(info.url), 100 | info, 101 | })) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/auto-updater/electron-main/update.service.ts: -------------------------------------------------------------------------------- 1 | import { dialog, BrowserWindow, MessageBoxOptions } from 'electron' 2 | import { Autowired, Injectable } from '@opensumi/di' 3 | import { ElectronMainApiProvider } from '@opensumi/ide-core-electron-main' 4 | import { ILogService } from '@/logger/common' 5 | import { UpdateWindow } from './update-window' 6 | import { IUpdateMainService, UpdateState } from '../common' 7 | import { AutoUpdaterService } from './auto-updater.service' 8 | 9 | @Injectable() 10 | export class UpdateMainService extends ElectronMainApiProvider implements IUpdateMainService { 11 | @Autowired(AutoUpdaterService) 12 | private updaterService: AutoUpdaterService 13 | 14 | @Autowired(ILogService) 15 | logger: ILogService 16 | 17 | @Autowired(UpdateWindow) 18 | updateWindow: UpdateWindow 19 | 20 | #checkTimer: NodeJS.Timeout | null = null 21 | 22 | async checkForUpdatesManual(): Promise { 23 | this.checkForUpdates({ manual: true }) 24 | } 25 | 26 | async checkForUpdatesAuto(): Promise { 27 | if (this.#checkTimer) { 28 | clearTimeout(this.#checkTimer) 29 | } 30 | const loopCheck = (first = true) => { 31 | this.#checkTimer = setTimeout(() => { 32 | this.checkForUpdates({ manual: false }) 33 | .then(() => { 34 | loopCheck(false) 35 | }) 36 | }, (first ? 10 : 3600) * 1000) 37 | } 38 | loopCheck() 39 | } 40 | 41 | async checkForUpdates({ manual = false }) { 42 | let { updateState } = this.updaterService 43 | this.logger.debug(`[auto-updater] checkForUpdates: ${manual ? '手动更新' : '自动更新'}, current updateState: ${updateState}`) 44 | 45 | if ( 46 | updateState === UpdateState.NoAvailable || 47 | updateState === UpdateState.CheckingError 48 | ) { 49 | await this.updaterService.checkForUpdates(); 50 | ({ updateState } = this.updaterService) 51 | } 52 | 53 | switch (updateState) { 54 | case UpdateState.Available: 55 | if (this.#checkTimer) { 56 | clearTimeout(this.#checkTimer) 57 | this.#checkTimer = null; 58 | } 59 | if (manual || !this.updaterService.ignoreVersion.has(this.updaterService.updateInfo?.version!)) { 60 | this.updateWindow.openWindow() 61 | } 62 | break; 63 | case UpdateState.NoAvailable: 64 | if (manual) { 65 | await this.showCheckDialog({ 66 | type: 'info', 67 | message: '当前没有可用的更新', 68 | buttons: ['确认'] 69 | }) 70 | } 71 | break; 72 | case UpdateState.CheckingError: 73 | if (manual) { 74 | await this.showCheckDialog({ 75 | type: 'info', 76 | message: '检查更新出错,请稍后重试', 77 | buttons: ['确认'] 78 | }) 79 | } 80 | break; 81 | default: 82 | if (manual) { 83 | this.updateWindow.openWindow() 84 | } 85 | break; 86 | } 87 | } 88 | 89 | private async showCheckDialog(options: MessageBoxOptions) { 90 | const browserWindow = BrowserWindow.getFocusedWindow() 91 | if (browserWindow) { 92 | await dialog.showMessageBox(browserWindow, options) 93 | } else { 94 | await dialog.showMessageBox(options) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/auto-updater/update-window/UpdateView.tsx: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, shell } from 'electron' 2 | import React, { useEffect, useMemo, useState } from 'react' 3 | import { marked } from '@opensumi/ide-components/lib/utils'; 4 | import logo from '@/core/browser/assets/logo.svg' 5 | import styles from './style.module.less' 6 | import { IPC_CHANNEL, ProgressInfo, InitialState, UpdateInfo, UpdateState, EventData } from '../common' 7 | 8 | export const UpdateView = () => { 9 | const [ updateInfo, setUpdateInfo ] = useState() 10 | const [ progressInfo, setProgressInfo ] = useState(null) 11 | const [ updateState, setUpdateState ] = useState(null) 12 | const releaseHtml = useMemo(() => { 13 | const releaseNotes = updateInfo?.releaseNotes 14 | if (!releaseNotes) return '' 15 | const releaseNote = Array.isArray(releaseNotes) ? releaseNotes[0]?.note : releaseNotes as string 16 | return marked(releaseNote || '无更新日志') 17 | }, [updateInfo]) 18 | 19 | const progressPercent = useMemo(() => { 20 | return (progressInfo?.percent || 0).toFixed(2) 21 | }, [progressInfo]) 22 | 23 | const installApp = async () => { 24 | setUpdateState(UpdateState.Downloading) 25 | try { 26 | await ipcRenderer.invoke(IPC_CHANNEL.downloadAndInstall) 27 | setUpdateState(UpdateState.Downloaded) 28 | } catch { 29 | setUpdateState(UpdateState.DownloadError) 30 | } 31 | } 32 | 33 | const ignoreVersion = () => { 34 | ipcRenderer.send(IPC_CHANNEL.ignoreVersion) 35 | } 36 | 37 | useEffect(() => { 38 | ipcRenderer.invoke(IPC_CHANNEL.initialState) 39 | .then((initialData: InitialState) => { 40 | setUpdateState(initialData.updateState) 41 | setProgressInfo(initialData.progressInfo) 42 | setUpdateInfo(initialData.updateInfo) 43 | }) 44 | .catch(() => { 45 | setUpdateInfo(null) 46 | }) 47 | 48 | ipcRenderer.on(IPC_CHANNEL.eventData, (event, data: EventData) => { 49 | if (data.event === 'download-progress') { 50 | setProgressInfo(data.data) 51 | } else if (data.event === 'error') { 52 | setUpdateState(UpdateState.UpdateError) 53 | } 54 | }) 55 | }, []) 56 | 57 | if (typeof updateInfo === 'undefined') return null 58 | 59 | if (updateInfo === null) { 60 | return ( 61 |
62 | 获取更新信息失败,请稍后重试 63 |
64 | ); 65 | } 66 | 67 | return ( 68 |
69 |
70 | logo 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/auto-updater/update-window/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CodeFuse IDE Update 6 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/auto-updater/update-window/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { UpdateView } from './UpdateView' 4 | 5 | createRoot(document.getElementById('main')!).render() 6 | -------------------------------------------------------------------------------- /src/auto-updater/update-window/style.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | min-height: 0; 4 | font-size: 14px; 5 | padding: 32px; 6 | height: 100%; 7 | box-sizing: border-box; 8 | &.error { 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | } 13 | 14 | .icon { 15 | width: 52px; 16 | height: 52px; 17 | img { 18 | width: 100%; 19 | } 20 | } 21 | 22 | .body { 23 | display: flex; 24 | flex: 1; 25 | flex-direction: column; 26 | padding-left: 20px; 27 | } 28 | 29 | .title { 30 | margin-bottom: 4px; 31 | } 32 | 33 | .subtitle { 34 | margin-bottom: 8px; 35 | } 36 | 37 | .changelogTitle { 38 | margin-bottom: 12px; 39 | } 40 | 41 | .pageLoading { 42 | display: flex; 43 | flex-direction: column; 44 | height: 100%; 45 | align-items: center; 46 | justify-content: center; 47 | } 48 | 49 | .changelog { 50 | padding: 16px; 51 | flex: 1; 52 | min-height: 0; 53 | overflow: auto; 54 | border-radius: 8px; 55 | background-color: #f0f2f5; 56 | margin-bottom: 16px; 57 | li { 58 | margin: 0; 59 | padding: 0; 60 | } 61 | ul { 62 | padding-left: 15px; 63 | } 64 | } 65 | 66 | .footer { 67 | display: flex; 68 | justify-content: space-between; 69 | align-items: center; 70 | button { 71 | border: none; 72 | outline: none; 73 | cursor: pointer; 74 | display: inline-flex; 75 | justify-content: center; 76 | align-items: center; 77 | padding: 0px 12px; 78 | height: 28px; 79 | line-height: 1; 80 | box-sizing: border-box; 81 | border-radius: 4px; 82 | white-space: pre-wrap; 83 | color: #000a1aff; 84 | background-color: #fafafb; 85 | &:hover { 86 | background-color: #F5F5F6; 87 | } 88 | &.installBtn { 89 | color: #FFF; 90 | margin-left: 8px; 91 | background-color: #3c8dff; 92 | &:hover { 93 | background-color: rgba(60, 141, 255, 0.65); 94 | } 95 | &:disabled { 96 | background-color: rgba(21, 27, 33, 0.04); 97 | color: rgba(21, 27, 33, 0.35); 98 | cursor: not-allowed; 99 | } 100 | } 101 | } 102 | .progress { 103 | &.error { 104 | color: rgb(218, 80, 21); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /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/bootstrap-web/browser/core-commands.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Autowired } from '@opensumi/di'; 2 | import { CommandContribution, CommandRegistry, Domain, FILE_COMMANDS } from '@opensumi/ide-core-browser'; 3 | import { IWindowDialogService } from '@opensumi/ide-overlay'; 4 | import { IWorkspaceService } from '@opensumi/ide-workspace'; 5 | 6 | @Injectable() 7 | @Domain(CommandContribution) 8 | export class CoreCommandContribution implements CommandContribution { 9 | @Autowired(IWindowDialogService) 10 | private window: IWindowDialogService; 11 | 12 | @Autowired(IWorkspaceService) 13 | private workspace: IWorkspaceService; 14 | 15 | registerCommands(commands: CommandRegistry) { 16 | commands.registerCommand(FILE_COMMANDS.OPEN_FOLDER, { 17 | execute: async () => { 18 | const newWorkspace = await this.window.showOpenDialog({ 19 | canSelectFolders: true, 20 | canSelectMany: false, 21 | }); 22 | if (newWorkspace) { 23 | if (this.workspace.workspace?.uri.toString() === newWorkspace[0].toString()) { 24 | return; 25 | } 26 | window.open(`${window.location.protocol}//${window.location.host}?workspaceDir=${newWorkspace[0].codeUri.fsPath.toString()}`); 27 | } 28 | } 29 | }) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/bootstrap-web/browser/index.ts: -------------------------------------------------------------------------------- 1 | import '@opensumi/ide-i18n/lib/browser'; 2 | import {ExpressFileServerModule} from '@opensumi/ide-express-file-server/lib/browser'; 3 | import '@opensumi/ide-core-browser/lib/style/index.less'; 4 | import '@opensumi/ide-core-browser/lib/style/icon.less'; 5 | 6 | import {renderApp} from './render-app'; 7 | import {CommonBrowserModules} from '@/bootstrap-web/browser/common-modules'; 8 | import {layoutConfig} from './layout-config'; 9 | import './main.less'; 10 | import './styles.less'; 11 | import {AILayout} from "@opensumi/ide-ai-native/lib/browser/layout/ai-layout"; 12 | import {DEFAULT_LAYOUT_VIEW_SIZE} from "@opensumi/ide-core-browser/lib/layout/constants"; 13 | import {AINativeSettingSectionsId} from "@opensumi/ide-core-common"; 14 | import logo from '@/core/browser/assets/logo.svg' 15 | 16 | 17 | renderApp({ 18 | modules: [ 19 | ...CommonBrowserModules, 20 | ExpressFileServerModule, 21 | ], 22 | layoutConfig, 23 | layoutComponent: AILayout, 24 | layoutViewSize: { 25 | bigSurTitleBarHeight: DEFAULT_LAYOUT_VIEW_SIZE.menubarHeight, 26 | }, 27 | useCdnIcon: false, 28 | useExperimentalShadowDom: false, 29 | defaultPreferences: { 30 | 'settings.userBeforeWorkspace': true, 31 | 'general.icon': 'vs-seti', 32 | [AINativeSettingSectionsId.IntelligentCompletionsPromptEngineeringEnabled]: false, 33 | // 总是显示智能提示 34 | [AINativeSettingSectionsId.IntelligentCompletionsAlwaysVisible]: true, 35 | }, 36 | AINativeConfig: { 37 | layout: { 38 | menubarLogo: logo, 39 | } 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /src/bootstrap-web/browser/layout-config.ts: -------------------------------------------------------------------------------- 1 | import { SlotLocation } from '@opensumi/ide-core-browser/lib/react-providers/slot'; 2 | import { defaultConfig } from '@opensumi/ide-main-layout/lib/browser/default-config'; 3 | import { DESIGN_MENUBAR_CONTAINER_VIEW_ID } from '@opensumi/ide-design/lib/common/constants'; 4 | import {DESIGN_MENU_BAR_LEFT} from "@opensumi/ide-design"; 5 | import {AI_MENU_BAR_LEFT_ACTION} from "@/ai/browser"; 6 | 7 | export const layoutConfig = { 8 | ...defaultConfig, 9 | [SlotLocation.top]: { 10 | modules: [DESIGN_MENUBAR_CONTAINER_VIEW_ID], 11 | }, 12 | [SlotLocation.bottom]: { 13 | modules: [ 14 | '@opensumi/ide-terminal-next', 15 | '@opensumi/ide-output', 16 | 'debug-console', 17 | '@opensumi/ide-markers', 18 | '@opensumi/ide-refactor-preview', 19 | ], 20 | }, 21 | 22 | [DESIGN_MENU_BAR_LEFT]: { 23 | modules: [AI_MENU_BAR_LEFT_ACTION] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/bootstrap-web/browser/main.less: -------------------------------------------------------------------------------- 1 | :root { 2 | --tabBar-height: 35px !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/bootstrap-web/browser/render-app.ts: -------------------------------------------------------------------------------- 1 | import {Injector} from '@opensumi/di'; 2 | import {IClientAppOpts} from '@opensumi/ide-core-browser'; 3 | import {ClientApp} from '@opensumi/ide-core-browser/lib/bootstrap/app'; 4 | import {ToolbarActionBasedLayout} from '@opensumi/ide-core-browser/lib/components'; 5 | import logo from '@/core/browser/assets/logo.svg' 6 | import {CoreCommandContribution} from "@/bootstrap-web/browser/core-commands"; 7 | 8 | export async function renderApp(opts: IClientAppOpts) { 9 | const injector = new Injector(); 10 | injector.addProviders(CoreCommandContribution); 11 | 12 | const hostname = window.location.hostname; 13 | const query = new URLSearchParams(window.location.search); 14 | // 线上的静态服务和 IDE 后端是一个 Server 15 | const serverPort = process.env.DEVELOPMENT ? 8000 : window.location.port; 16 | const staticServerPort = process.env.DEVELOPMENT ? 8080 : window.location.port; 17 | const webviewEndpointPort = process.env.DEVELOPMENT ? 8899 : window.location.port; 18 | opts.appName= 'CodeFuse IDE'; 19 | opts.workspaceDir = opts.workspaceDir || query.get('workspaceDir') || process.env.WORKSPACE_DIR; 20 | 21 | opts.extensionDir = opts.extensionDir || process.env.EXTENSION_DIR; 22 | 23 | opts.wsPath = process.env.WS_PATH || (window.location.protocol == 'https:' ? `wss://${hostname}:${serverPort}` : `ws://${hostname}:${serverPort}`); 24 | console.log(opts.wsPath) 25 | opts.extWorkerHost = opts.extWorkerHost || process.env.EXTENSION_WORKER_HOST || `http://${hostname}:${staticServerPort}/ext-host/worker-host.js`; 26 | opts.staticServicePath = `http://${hostname}:${serverPort}`; 27 | const anotherHostName = process.env.WEBVIEW_HOST || hostname; 28 | opts.webviewEndpoint = `http://${anotherHostName}:${webviewEndpointPort}/webview`; 29 | opts.layoutComponent = opts.layoutComponent || ToolbarActionBasedLayout; 30 | opts.injector = injector 31 | opts.isElectronRenderer = false 32 | opts.AINativeConfig = { 33 | layout: { 34 | menubarLogo: logo, 35 | } 36 | } 37 | const app = new ClientApp(opts); 38 | 39 | app.fireOnReload = () => { 40 | window.location.reload(); 41 | }; 42 | 43 | app.start(document.getElementById('main')!, 'web'); 44 | } 45 | -------------------------------------------------------------------------------- /src/bootstrap-web/browser/styles.less: -------------------------------------------------------------------------------- 1 | @import '~@opensumi/ide-core-browser/lib/style/variable.less'; 2 | 3 | #main { 4 | display: flex; 5 | flex-direction: column; 6 | width: 100vw; 7 | height: 100vh; 8 | margin: 0; 9 | padding: 0; 10 | overflow: hidden; 11 | background-color: var(--background); 12 | } 13 | -------------------------------------------------------------------------------- /src/bootstrap-web/common/index.ts: -------------------------------------------------------------------------------- 1 | import {config} from 'dotenv' 2 | import path from "path"; 3 | 4 | config({ 5 | path: path.resolve(__dirname, '../../../..env.sample') 6 | }) 7 | -------------------------------------------------------------------------------- /src/bootstrap-web/ext-host/index.ts: -------------------------------------------------------------------------------- 1 | import '../common' 2 | 3 | import '@/core/common/asar' 4 | import { extProcessInit } from '@opensumi/ide-extension/lib/hosted/ext.process-base.js'; 5 | import { Injector } from '@opensumi/di'; 6 | import { LogServiceManager } from '@/logger/node/log-manager'; 7 | import { LogServiceManager as LogServiceManagerToken } from '@opensumi/ide-logs/lib/node/log-manager'; 8 | 9 | const injector = new Injector() 10 | injector.addProviders( 11 | { 12 | token: LogServiceManagerToken, 13 | useClass: LogServiceManager 14 | }, 15 | ) 16 | 17 | extProcessInit({ 18 | injector, 19 | }) 20 | -------------------------------------------------------------------------------- /src/bootstrap-web/ext-host/index.worker.ts: -------------------------------------------------------------------------------- 1 | import '@opensumi/ide-extension/lib/hosted/worker.host-preload'; 2 | -------------------------------------------------------------------------------- /src/bootstrap-web/node/common-modules.ts: -------------------------------------------------------------------------------- 1 | import { NodeModule, ConstructorOf } from '@opensumi/ide-core-node'; 2 | import { ServerCommonModule } from '@opensumi/ide-core-node'; 3 | import { FileServiceModule } from '@opensumi/ide-file-service/lib/node'; 4 | import { OpenerModule } from '@opensumi/ide-remote-opener/lib/node'; 5 | import { ProcessModule } from '@opensumi/ide-process/lib/node'; 6 | import { FileSearchModule } from '@opensumi/ide-file-search/lib/node'; 7 | import { SearchModule } from '@opensumi/ide-search/lib/node'; 8 | import { TerminalNodePtyModule } from '@opensumi/ide-terminal-next/lib/node'; 9 | import { LogServiceModule } from '@opensumi/ide-logs/lib/node'; 10 | import { ExtensionModule } from '@opensumi/ide-extension/lib/node'; 11 | import { OpenVsxExtensionManagerModule } from '@opensumi/ide-extension-manager/lib/node'; 12 | import { FileSchemeNodeModule } from '@opensumi/ide-file-scheme/lib/node'; 13 | import { AddonsModule } from '@opensumi/ide-addons/lib/node'; 14 | import {CoreNodeModule} from "@/core/node"; 15 | import {LoggerModule} from "@/logger/node"; 16 | import {AINativeModule} from "@opensumi/ide-ai-native/lib/node"; 17 | import {AIServiceModule} from "@/ai/node"; 18 | 19 | export const CommonNodeModules: ConstructorOf[] = [ 20 | ServerCommonModule, 21 | LogServiceModule, 22 | FileServiceModule, 23 | ProcessModule, 24 | FileSearchModule, 25 | SearchModule, 26 | TerminalNodePtyModule, 27 | ExtensionModule, 28 | OpenVsxExtensionManagerModule, 29 | FileSchemeNodeModule, 30 | AddonsModule, 31 | CoreNodeModule, 32 | LoggerModule, 33 | OpenerModule, 34 | // ai 35 | AINativeModule, 36 | AIServiceModule, 37 | ]; 38 | -------------------------------------------------------------------------------- /src/bootstrap-web/node/index.ts: -------------------------------------------------------------------------------- 1 | import '../common' 2 | 3 | import {startServer} from './start-server'; 4 | import {ExpressFileServerModule} from '@opensumi/ide-express-file-server/lib/node'; 5 | import {CommonNodeModules} from './common-modules'; 6 | 7 | startServer({ 8 | modules: [ 9 | ...CommonNodeModules, 10 | ExpressFileServerModule, 11 | ], 12 | }); 13 | -------------------------------------------------------------------------------- /src/bootstrap-web/node/start-server.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as http from 'http'; 3 | import Koa from 'koa'; 4 | import koaStatic from 'koa-static'; 5 | import { Deferred } from '@opensumi/ide-core-common'; 6 | import { IServerAppOpts, ServerApp, NodeModule } from '@opensumi/ide-core-node'; 7 | 8 | export async function startServer(arg1: NodeModule[] | Partial) { 9 | const app = new Koa(); 10 | const deferred = new Deferred(); 11 | process.env.EXT_MODE = 'js'; 12 | const port = process.env.IDE_SERVER_PORT || 8000; 13 | const workspaceDir = process.env.WORKSPACE_DIR || process.env.NODE_ENV === 'production' ? path.join(__dirname, '../../workspace') : path.join(__dirname, '../../../workspace'); 14 | const extensionDir = process.env.EXTENSION_DIR || process.env.NODE_ENV === 'production' ? path.join(__dirname, '../../extensions') : path.join(__dirname, '../../../extensions'); 15 | const extensionHost = process.env.EXTENSION_HOST_ENTRY || 16 | process.env.NODE_ENV === 'production' ? path.join(__dirname, '..', '..', 'out/ext-host/index.js') : path.join(__dirname, '..', '..', '..', 'out/ext-host/index.js'); 17 | 18 | let opts: IServerAppOpts = { 19 | use: app.use.bind(app), 20 | processCloseExitThreshold: 5 * 60 * 1000, 21 | terminalPtyCloseThreshold: 5 * 60 * 1000, 22 | staticAllowOrigin: '*', 23 | staticAllowPath: [ 24 | workspaceDir, 25 | extensionDir, 26 | '/', 27 | ], 28 | extHost: extensionHost, 29 | }; 30 | 31 | opts.marketplace = { 32 | showBuiltinExtensions: true, 33 | } 34 | 35 | if (Array.isArray(arg1)) { 36 | opts = { 37 | ...opts, 38 | modulesInstances: arg1, 39 | }; 40 | } else { 41 | opts = { 42 | ...opts, 43 | ...arg1, 44 | }; 45 | } 46 | 47 | const serverApp = new ServerApp(opts); 48 | const server = http.createServer(app.callback()); 49 | 50 | if (process.env.NODE_ENV === 'production') { 51 | app.use(koaStatic(path.join(__dirname, '../../out'))); 52 | } 53 | 54 | await serverApp.start(server); 55 | 56 | server.on('error', (err) => { 57 | deferred.reject(err); 58 | console.error('Server error: ' + err.message); 59 | setTimeout(process.exit, 0, 1); 60 | }); 61 | 62 | server.listen(port, () => { 63 | console.log(`Server listen on port ${port}`); 64 | deferred.resolve(server); 65 | }); 66 | return deferred.promise; 67 | } 68 | -------------------------------------------------------------------------------- /src/bootstrap/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | Codefuse IDE 17 | 27 | 28 | 29 |
30 | 31 | 32 | -------------------------------------------------------------------------------- /src/bootstrap/browser/index.less: -------------------------------------------------------------------------------- 1 | select.kt_select { 2 | background-color: var(--editor-background); 3 | } 4 | 5 | quick-open-container { 6 | top: 36px !important; 7 | } 8 | -------------------------------------------------------------------------------- /src/bootstrap/browser/preload.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const os = require('os'); 3 | const path = require('path') 4 | 5 | const { ipcRenderer } = require('electron'); 6 | 7 | const electronEnv = {}; 8 | 9 | const urlParams = new URLSearchParams(decodeURIComponent(window.location.search)); 10 | window.id = Number(urlParams.get('windowId')); 11 | const webContentsId = Number(urlParams.get('webContentsId')); 12 | 13 | function createRPCNetConnection() { 14 | const rpcListenPath = ipcRenderer.sendSync('window-rpc-listen-path', electronEnv.currentWindowId); 15 | return net.createConnection(rpcListenPath); 16 | } 17 | 18 | function createNetConnection(connectPath) { 19 | return net.createConnection(connectPath); 20 | } 21 | 22 | electronEnv.ElectronIpcRenderer = ipcRenderer; 23 | electronEnv.createNetConnection = createNetConnection; 24 | electronEnv.createRPCNetConnection = createRPCNetConnection; 25 | 26 | electronEnv.platform = os.platform(); 27 | electronEnv.osRelease = os.release(); 28 | 29 | electronEnv.isElectronRenderer = true; 30 | electronEnv.BufferBridge = Buffer; 31 | electronEnv.currentWindowId = window.id; 32 | electronEnv.currentWebContentsId = webContentsId; 33 | electronEnv.monacoWorkerPath = path.join(__dirname, 'editor.worker.bundle.js'); 34 | 35 | const metaData = JSON.parse(ipcRenderer.sendSync('window-metadata', electronEnv.currentWindowId)); 36 | electronEnv.metadata = metaData; 37 | process.env = Object.assign({}, process.env, metaData.env, { WORKSPACE_DIR: metaData.workspace }); 38 | 39 | electronEnv.onigWasmPath = path.join(__dirname, '..', '..', '..', metaData.environment.isDev ? 'node_modules' : 'node_modules.asar.unpacked', 'vscode-oniguruma/release/onig.wasm' ); 40 | electronEnv.treeSitterWasmDirectoryPath = path.join(__dirname, '..', '..', '..', metaData.environment.isDev ? 'node_modules' : 'node_modules.asar.unpacked', '@opensumi/tree-sitter-wasm' ); 41 | electronEnv.appPath = metaData.appPath; 42 | electronEnv.env = Object.assign({}, process.env); 43 | electronEnv.webviewPreload = metaData.webview.webviewPreload; 44 | electronEnv.plainWebviewPreload = metaData.webview.plainWebviewPreload; 45 | electronEnv.env.EXTENSION_DIR = metaData.extensionDir[0]; 46 | 47 | global.electronEnv = electronEnv; 48 | Object.assign(global, electronEnv); 49 | 50 | if (metaData.preloads) { 51 | metaData.preloads.forEach((preload) => { 52 | require(preload); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /src/bootstrap/electron-main/index.ts: -------------------------------------------------------------------------------- 1 | import '@/core/common/asar' 2 | import '@/i18n' 3 | import { app } from 'electron'; 4 | import * as path from 'node:path'; 5 | import { URI } from '@opensumi/ide-core-common' 6 | import { WebviewElectronMainModule } from '@opensumi/ide-webview/lib/electron-main'; 7 | import { ElectronMainApp } from '@/core/electron-main' 8 | import { CoreElectronMainModule } from '@/core/electron-main'; 9 | import { LoggerModule } from '@/logger/electron-main' 10 | import { AutoUpdaterModule } from '@/auto-updater/electron-main' 11 | 12 | const modules = [ 13 | CoreElectronMainModule, 14 | WebviewElectronMainModule, 15 | LoggerModule, 16 | AutoUpdaterModule, 17 | ] 18 | 19 | startMain(); 20 | 21 | function startMain() { 22 | const mainApp = new ElectronMainApp({ 23 | modules, 24 | browserUrl: __CODE_WINDOW_DEV_SERVER_URL__ || URI.file(path.join(__dirname, `../renderer/${__CODE_WINDOW_NAME__}/index.html`)).toString(), 25 | browserPreload: path.resolve(__dirname, `../renderer/${__CODE_WINDOW_NAME__}/preload.js`), 26 | nodeEntry: path.join(__dirname, '../node/index.js'), 27 | extensionEntry: path.join(__dirname, '../ext-host/index.js'), 28 | extensionWorkerEntry: path.join(__dirname, '../ext-host/worker-host.js'), 29 | webviewPreload: path.join(__dirname, '../webview/host-preload.js'), 30 | plainWebviewPreload: path.join(__dirname, '../webview/plain-preload.js'), 31 | extensionDir: path.join(app.getAppPath(), 'extensions'), 32 | extensionCandidate: [], 33 | browserNodeIntegrated: true, 34 | }) 35 | 36 | mainApp.start(); 37 | } 38 | -------------------------------------------------------------------------------- /src/bootstrap/ext-host/index.ts: -------------------------------------------------------------------------------- 1 | import '@/core/common/asar' 2 | import { extProcessInit } from '@opensumi/ide-extension/lib/hosted/ext.process-base.js'; 3 | import { Injector } from '@opensumi/di'; 4 | import { LogServiceManager } from '@/logger/node/log-manager'; 5 | import { LogServiceManager as LogServiceManagerToken } from '@opensumi/ide-logs/lib/node/log-manager'; 6 | 7 | const injector = new Injector() 8 | injector.addProviders( 9 | { 10 | token: LogServiceManagerToken, 11 | useClass: LogServiceManager 12 | }, 13 | ) 14 | 15 | extProcessInit({ 16 | injector, 17 | }) 18 | -------------------------------------------------------------------------------- /src/bootstrap/ext-host/index.worker.ts: -------------------------------------------------------------------------------- 1 | import '@opensumi/ide-extension/lib/hosted/worker.host-preload'; 2 | -------------------------------------------------------------------------------- /src/bootstrap/node/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import '@/core/common/asar' 3 | import * as net from 'node:net'; 4 | import path from 'node:path'; 5 | import mri from 'mri' 6 | import { IServerAppOpts, ServerApp, ConstructorOf, NodeModule } from '@opensumi/ide-core-node'; 7 | import { ServerCommonModule } from '@opensumi/ide-core-node'; 8 | import { FileServiceModule } from '@opensumi/ide-file-service/lib/node'; 9 | import { ProcessModule } from '@opensumi/ide-process/lib/node'; 10 | import { FileSearchModule } from '@opensumi/ide-file-search/lib/node'; 11 | import { SearchModule } from '@opensumi/ide-search/lib/node'; 12 | import { TerminalNodePtyModule } from '@opensumi/ide-terminal-next/lib/node'; 13 | import { terminalPreferenceSchema } from '@opensumi/ide-terminal-next/lib/common/preference' 14 | import { LogServiceModule } from '@opensumi/ide-logs/lib/node'; 15 | import { ExtensionModule } from '@opensumi/ide-extension/lib/node'; 16 | import { FileSchemeNodeModule } from '@opensumi/ide-file-scheme/lib/node'; 17 | import { AddonsModule } from '@opensumi/ide-addons/lib/node'; 18 | import { OpenVsxExtensionManagerModule } from '@opensumi/ide-extension-manager/lib/node'; 19 | import { AINativeModule } from '@opensumi/ide-ai-native/lib/node'; 20 | import { CoreNodeModule } from '@/core/node'; 21 | import { LoggerModule } from '@/logger/node' 22 | import { AIServiceModule } from '@/ai/node'; 23 | 24 | const modules: ConstructorOf[] = [ 25 | ServerCommonModule, 26 | LogServiceModule, 27 | FileServiceModule, 28 | ProcessModule, 29 | FileSearchModule, 30 | SearchModule, 31 | TerminalNodePtyModule, 32 | ExtensionModule, 33 | OpenVsxExtensionManagerModule, 34 | FileSchemeNodeModule, 35 | AddonsModule, 36 | CoreNodeModule, 37 | LoggerModule, 38 | // ai 39 | AINativeModule, 40 | AIServiceModule, 41 | ] 42 | 43 | startServer(); 44 | 45 | async function startServer() { 46 | const opts: IServerAppOpts = { 47 | modules, 48 | webSocketHandler: [], 49 | marketplace: { 50 | showBuiltinExtensions: true, 51 | extensionDir: process.env.IDE_EXTENSIONS_PATH!, 52 | }, 53 | watcherHost: path.join(__dirname, '../watcher-host/index'), 54 | }; 55 | 56 | const server = net.createServer(); 57 | const serverApp = new ServerApp(opts); 58 | await serverApp.start(server); 59 | 60 | server.on('error', () => { 61 | setTimeout(() => { 62 | process.exit(1); 63 | }); 64 | }); 65 | 66 | const listenPath = mri(process.argv).listenPath; 67 | server.listen(listenPath, () => { 68 | process.send?.('ready'); 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /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/core/browser/header/header.contribution.ts: -------------------------------------------------------------------------------- 1 | import { Domain } from '@opensumi/ide-core-browser'; 2 | import { ComponentContribution, ComponentRegistry } from '@opensumi/ide-core-browser/lib/layout'; 3 | import { ElectronHeaderBar } from './header.view' 4 | 5 | export const ELECTRON_HEADER = 'electron_header'; 6 | export const WINDOW = 'electron_header'; 7 | 8 | @Domain(ComponentContribution) 9 | export class HeaderContribution implements ComponentContribution { 10 | registerComponent(registry: ComponentRegistry): void { 11 | registry.register( 12 | ELECTRON_HEADER, 13 | { 14 | id: ELECTRON_HEADER, 15 | component: ElectronHeaderBar, 16 | }, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/browser/header/header.module.less: -------------------------------------------------------------------------------- 1 | :global(#top) { 2 | position: relative; 3 | } 4 | 5 | .header { 6 | position: absolute; 7 | background: var(--kt-menu-background); 8 | color: var(--titlebar-activeForeground); 9 | display: flex; 10 | align-items: center; 11 | text-align: center; 12 | line-height: 100%; 13 | font-size: 12px; 14 | user-select: none; 15 | 16 | .title_info { 17 | height: 100%; 18 | display: flex; 19 | align-items: center; 20 | -webkit-app-region: drag; 21 | flex-grow: 1; 22 | text-align: left; 23 | } 24 | 25 | :global .menu-bar { 26 | flex-shrink: 0; 27 | } 28 | 29 | :global .menubarWrapper { 30 | background-color: var(--menu-background); 31 | } 32 | 33 | .windowActions { 34 | display: flex; 35 | align-items: center; 36 | -webkit-app-region: no-drag; 37 | user-select: none; 38 | 39 | > .icon { 40 | font-size: 16px; 41 | padding: 0 15px; 42 | height: 100%; 43 | width: 100%; 44 | display: flex; 45 | align-items: center; 46 | 47 | &:hover { 48 | background-color: hsla(0, 0%, 100%, 0.1); 49 | } 50 | 51 | &:hover:last-child { 52 | background-color: rgba(232, 17, 35, 0.9); 53 | color: white; 54 | } 55 | } 56 | } 57 | } 58 | 59 | :global(.vs) { 60 | .header { 61 | .windowActions { 62 | > .icon:not(:last-child):hover { 63 | background-color: rgba(0, 0, 0, 0.1); 64 | color: #696767; 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /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/core/browser/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule, createElectronMainApi, IElectronNativeDialogService } from '@opensumi/ide-core-browser'; 2 | import { Injectable } from '@opensumi/di'; 3 | import { ElectronBasicContribution } from '@opensumi/ide-electron-basic/lib/browser' 4 | import { ElectronNativeDialogService } from '@opensumi/ide-electron-basic/lib/browser/dialog' 5 | import { ElectronHeaderService } from '@opensumi/ide-electron-basic/lib/browser/header/header.service' 6 | import { ElectronPreferenceContribution } from '@opensumi/ide-electron-basic/lib/browser/electron-preference.contribution' 7 | import { IElectronHeaderService } from '@opensumi/ide-electron-basic/lib/common/header' 8 | 9 | import { ProjectSwitcherContribution } from './project.contribution'; 10 | import { LocalMenuContribution } from './menu.contribution'; 11 | import { LocalThemeContribution } from './theme.contribution'; 12 | import { patchProviders } from './patch' 13 | import { IStorageService, IAppMenuService, IThemeService } from '../common'; 14 | import { HeaderContribution, ELECTRON_HEADER } from './header/header.contribution' 15 | import { WelcomeContribution } from './welcome/welcome.contribution' 16 | 17 | export { ELECTRON_HEADER } 18 | 19 | @Injectable() 20 | export class CoreBrowserModule extends BrowserModule { 21 | providers = [ 22 | { 23 | token: IElectronNativeDialogService, 24 | useClass: ElectronNativeDialogService, 25 | }, 26 | { 27 | token: IElectronHeaderService, 28 | useClass: ElectronHeaderService, 29 | }, 30 | ElectronBasicContribution, 31 | ElectronPreferenceContribution, 32 | WelcomeContribution, 33 | HeaderContribution, 34 | ProjectSwitcherContribution, 35 | LocalMenuContribution, 36 | LocalThemeContribution, 37 | { 38 | token: IStorageService, 39 | useValue: createElectronMainApi(IStorageService), 40 | }, 41 | { 42 | token: IThemeService, 43 | useValue: createElectronMainApi(IThemeService), 44 | }, 45 | { 46 | token: IAppMenuService, 47 | useValue: createElectronMainApi(IAppMenuService), 48 | }, 49 | ...patchProviders, 50 | ]; 51 | } 52 | -------------------------------------------------------------------------------- /src/core/browser/menu.contribution.ts: -------------------------------------------------------------------------------- 1 | import { Autowired } from '@opensumi/di' 2 | import { CommandContribution, CommandRegistry, Domain, MaybePromise } from '@opensumi/ide-core-common' 3 | import { ClientAppContribution, electronEnv } from '@opensumi/ide-core-browser' 4 | import { IMenuRegistry, MenuId, MenuContribution } from "@opensumi/ide-core-browser/lib/menu/next"; 5 | import { localize } from "@opensumi/ide-core-common/lib/localize"; 6 | import { IWorkspaceService } from '@opensumi/ide-workspace'; 7 | import { IAppMenuService } from '../common'; 8 | import { IElectronMainUIService } from '@opensumi/ide-core-common/lib/electron'; 9 | 10 | const OPEN_LOGO_DIR_COMMAND_ID = { 11 | id: 'codefuse-ide.openLogDir', 12 | label: localize('codefuse-ide.openLogDir'), 13 | } 14 | 15 | @Domain(ClientAppContribution, MenuContribution, CommandContribution) 16 | export class LocalMenuContribution implements MenuContribution, ClientAppContribution { 17 | @Autowired(IWorkspaceService) 18 | workspaceService: IWorkspaceService; 19 | 20 | @Autowired(IAppMenuService) 21 | menuService: IAppMenuService; 22 | 23 | @Autowired(IElectronMainUIService) 24 | private electronMainUIService: IElectronMainUIService; 25 | 26 | initialize(): MaybePromise { 27 | // this.renderAppMenu(); 28 | } 29 | 30 | async renderAppMenu() { 31 | const workspaces = await this.workspaceService.getMostRecentlyUsedWorkspaces(); 32 | await this.menuService.renderRecentWorkspaces(workspaces); 33 | } 34 | 35 | registerCommands(registry: CommandRegistry) { 36 | registry.registerCommand(OPEN_LOGO_DIR_COMMAND_ID, { 37 | execute: () => { 38 | this.electronMainUIService.revealInFinder(electronEnv.metadata.environment.logRoot); 39 | }, 40 | }); 41 | } 42 | 43 | registerMenus(menuRegistry: IMenuRegistry) { 44 | menuRegistry.registerMenuItem(MenuId.MenubarAppMenu, { 45 | submenu: MenuId.SettingsIconMenu, 46 | label: localize('common.preferences'), 47 | group: '2_preference', 48 | }); 49 | 50 | menuRegistry.registerMenuItem(MenuId.MenubarHelpMenu, { 51 | command: OPEN_LOGO_DIR_COMMAND_ID, 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/core/browser/patch.ts: -------------------------------------------------------------------------------- 1 | import { Autowired, Provider } from '@opensumi/di' 2 | import { Domain, Schemes, URI } from '@opensumi/ide-core-common' 3 | import { AppConfig, WorkspaceScope } from '@opensumi/ide-core-browser' 4 | import { IMenuRegistry, MenuId, MenuContribution, IMenuItem } from "@opensumi/ide-core-browser/lib/menu/next"; 5 | import { FILE_COMMANDS, ClientAppContribution, formatLocalize, StaticResourceContribution, StaticResourceService, electronEnv } from '@opensumi/ide-core-browser' 6 | import { IViewsRegistry } from '@opensumi/ide-main-layout'; 7 | import { RESOURCE_VIEW_ID } from '@opensumi/ide-file-tree-next' 8 | import { IPreferenceSettingsService } from '@opensumi/ide-core-browser/lib/preferences'; 9 | import { PreferenceSettingsService } from '@opensumi/ide-preferences/lib/browser/preference-settings.service' 10 | 11 | @Domain(ClientAppContribution, MenuContribution, StaticResourceContribution) 12 | export class PatchContribution implements MenuContribution, ClientAppContribution, StaticResourceContribution { 13 | @Autowired(IViewsRegistry) 14 | private viewsRegistry: IViewsRegistry; 15 | 16 | async onStart() { 17 | const viewContents = this.viewsRegistry.getViewWelcomeContent(RESOURCE_VIEW_ID); 18 | const openFolderContent = viewContents.find(item => item.content.includes(`(command:${FILE_COMMANDS.OPEN_FOLDER.id})`)) 19 | if (openFolderContent) { 20 | Object.assign(openFolderContent, { 21 | content: formatLocalize('welcome-view.noFolderHelp', `${FILE_COMMANDS.OPEN_FOLDER.id}?{"newWindow":false}`) 22 | }) 23 | } 24 | } 25 | 26 | registerMenus(menuRegistry: IMenuRegistry) { 27 | const openFolderMenu = menuRegistry.getMenuItems(MenuId.MenubarFileMenu).find(item => { 28 | return 'command' in item && item.command === FILE_COMMANDS.OPEN_FOLDER.id; 29 | }) as IMenuItem 30 | if (openFolderMenu) { 31 | openFolderMenu.extraTailArgs = [{ newWindow: false }] 32 | } 33 | } 34 | 35 | registerStaticResolver(service: StaticResourceService): void { 36 | service.registerStaticResourceProvider({ 37 | scheme: Schemes.monaco, 38 | resolveStaticResource: (uri) => { 39 | const path = uri.codeUri.path; 40 | 41 | switch (path) { 42 | case 'worker': { 43 | const query = uri.query; 44 | if (query) { 45 | const { moduleId } = JSON.parse(query); 46 | if (moduleId === 'workerMain.js') { 47 | return URI.file(electronEnv.monacoWorkerPath); 48 | } 49 | } 50 | break; 51 | } 52 | } 53 | 54 | return uri; 55 | }, 56 | }); 57 | } 58 | } 59 | 60 | export class PatchPreferenceSettingsService extends PreferenceSettingsService { 61 | @Autowired(AppConfig) 62 | appConfig: AppConfig 63 | 64 | constructor() { 65 | super() 66 | if (!this.appConfig.workspaceDir) { 67 | this.tabList = this.tabList.filter(item => item !== WorkspaceScope); 68 | } 69 | this._currentScope = this.tabList[0] 70 | } 71 | } 72 | 73 | export const patchProviders: Provider[] = [ 74 | PatchContribution, 75 | { 76 | token: IPreferenceSettingsService, 77 | useClass: PatchPreferenceSettingsService, 78 | override: true, 79 | } 80 | ] 81 | -------------------------------------------------------------------------------- /src/core/browser/project.contribution.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Domain, 3 | CommandContribution, 4 | CommandRegistry, 5 | URI, 6 | electronEnv, 7 | ClientAppContribution, 8 | StorageProvider, 9 | FILE_COMMANDS, 10 | } from '@opensumi/ide-core-browser'; 11 | import { IMenuRegistry, MenuId, MenuContribution } from '@opensumi/ide-core-browser/lib/menu/next'; 12 | import { Autowired } from '@opensumi/di'; 13 | import { IWorkspaceService } from '@opensumi/ide-workspace/lib/common'; 14 | import { IWindowService, WORKSPACE_COMMANDS } from '@opensumi/ide-core-browser'; 15 | import { ITerminalController } from '@opensumi/ide-terminal-next'; 16 | import { IMainLayoutService } from '@opensumi/ide-main-layout'; 17 | import { BrowserEditorContribution, WorkbenchEditorService } from '@opensumi/ide-editor/lib/browser'; 18 | import { IThemeService } from '@opensumi/ide-theme'; 19 | 20 | @Domain(MenuContribution, BrowserEditorContribution, ClientAppContribution) 21 | export class ProjectSwitcherContribution 22 | implements MenuContribution, BrowserEditorContribution, ClientAppContribution 23 | { 24 | @Autowired(IWorkspaceService) 25 | workspaceService: IWorkspaceService; 26 | 27 | @Autowired(IWindowService) 28 | windowService: IWindowService; 29 | 30 | @Autowired(ITerminalController) 31 | terminalService: ITerminalController; 32 | 33 | @Autowired(WorkbenchEditorService) 34 | editorService: WorkbenchEditorService; 35 | 36 | @Autowired(IMainLayoutService) 37 | private mainLayoutService: IMainLayoutService; 38 | 39 | @Autowired(IThemeService) 40 | private themeService: IThemeService; 41 | 42 | @Autowired(StorageProvider) 43 | getStorage: StorageProvider; 44 | 45 | async onStart() {} 46 | 47 | registerMenus(registry: IMenuRegistry) { 48 | registry.registerMenuItem(MenuId.MenubarFileMenu, { 49 | submenu: 'recentProjects', 50 | label: '最近项目', 51 | group: '1_open', 52 | }); 53 | 54 | this.workspaceService.getMostRecentlyUsedWorkspaces().then((workspaces) => { 55 | registry.registerMenuItems( 56 | 'recentProjects', 57 | workspaces.map((workspace) => ({ 58 | command: { 59 | id: FILE_COMMANDS.VSCODE_OPEN_FOLDER.id, 60 | label: new URI(workspace).codeUri.fsPath, 61 | }, 62 | extraTailArgs: [workspace, false], 63 | })), 64 | ); 65 | }); 66 | } 67 | 68 | onDidRestoreState() { 69 | if (electronEnv.metadata.launchToOpenFile) { 70 | this.editorService.open(URI.file(electronEnv.metadata.launchToOpenFile)); 71 | } 72 | electronEnv.ipcRenderer.on('openFile', (event, file) => { 73 | this.editorService.open(URI.file(file)); 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/core/browser/theme.contribution.ts: -------------------------------------------------------------------------------- 1 | import { Autowired } from '@opensumi/di'; 2 | import { ClientAppContribution } from '@opensumi/ide-core-browser/lib/common'; 3 | import { Domain, OnEvent, WithEventBus } from '@opensumi/ide-core-common'; 4 | import { ThemeChangedEvent } from '@opensumi/ide-theme/lib/common'; 5 | import { IThemeService } from '../common'; 6 | import { electronEnv } from '@opensumi/ide-core-browser'; 7 | 8 | @Domain(ClientAppContribution) 9 | export class LocalThemeContribution extends WithEventBus implements ClientAppContribution { 10 | @Autowired(IThemeService) 11 | private readonly themeService: IThemeService; 12 | 13 | initialize() {} 14 | 15 | @OnEvent(ThemeChangedEvent) 16 | onThemeChanged({ payload: { theme } }: ThemeChangedEvent) { 17 | this.themeService.setTheme(electronEnv.currentWindowId, { 18 | themeType: theme.type, 19 | menuBarBackground: theme.getColor('kt.menubar.background')?.toString(), 20 | sideBarBackground: theme.getColor('sideBar.background')?.toString(), 21 | editorBackground: theme.getColor('editor.background')?.toString(), 22 | panelBackground: theme.getColor('panel.background')?.toString(), 23 | statusBarBackground: theme.getColor('statusBar.background')?.toString(), 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/core/browser/welcome/common.ts: -------------------------------------------------------------------------------- 1 | export interface IWelcomeMetaData { 2 | recentWorkspaces: string[]; 3 | recentFiles: string[]; 4 | } -------------------------------------------------------------------------------- /src/core/browser/welcome/welcome.component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | CommandService, 5 | FILE_COMMANDS, 6 | FileUri, 7 | IWindowService, 8 | URI, 9 | localize, 10 | useInjectable, 11 | } from '@opensumi/ide-core-browser'; 12 | import { ReactEditorComponent } from '@opensumi/ide-editor/lib/browser'; 13 | import { IFileServiceClient } from '@opensumi/ide-file-service'; 14 | import { IMessageService } from '@opensumi/ide-overlay'; 15 | import { posix, win32 } from '@opensumi/ide-utils/lib/path' 16 | 17 | import { IWelcomeMetaData } from './common'; 18 | import styles from './welcome.module.less'; 19 | 20 | export const EditorWelcomeComponent: ReactEditorComponent = ({ resource }) => { 21 | const commandService: CommandService = useInjectable(CommandService); 22 | const windowService: IWindowService = useInjectable(IWindowService); 23 | const fileService: IFileServiceClient = useInjectable(IFileServiceClient); 24 | const messageService: IMessageService = useInjectable(IMessageService); 25 | 26 | return ( 27 |
28 | 40 |
41 |

{localize('welcome.recent.workspace')}

42 | {resource.metadata?.recentWorkspaces.map((workspace) => { 43 | let workspacePath = workspace; 44 | if (workspace.startsWith('file://')) { 45 | workspacePath = FileUri.fsPath(workspace); 46 | } 47 | const p = workspacePath.indexOf('/') !== -1 ? posix : win32; 48 | let name = p.basename(workspacePath); 49 | let parentPath = p.dirname(workspacePath); 50 | if (!name.length) { 51 | name = parentPath 52 | parentPath = '' 53 | } 54 | // only the root segment 55 | return ( 56 | 72 | ); 73 | })} 74 |
75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/core/browser/welcome/welcome.contribution.ts: -------------------------------------------------------------------------------- 1 | import { Autowired } from '@opensumi/di'; 2 | import { ClientAppContribution, Domain, RecentFilesManager, URI, localize } from '@opensumi/ide-core-browser'; 3 | import { IResource, ResourceService, WorkbenchEditorService } from '@opensumi/ide-editor'; 4 | import { 5 | BrowserEditorContribution, 6 | EditorComponentRegistry, 7 | EditorComponentRenderMode, 8 | EditorOpenType, 9 | } from '@opensumi/ide-editor/lib/browser'; 10 | import { IWorkspaceService } from '@opensumi/ide-workspace'; 11 | 12 | import { IWelcomeMetaData } from './common' 13 | import { EditorWelcomeComponent } from './welcome.component'; 14 | 15 | @Domain(BrowserEditorContribution, ClientAppContribution) 16 | export class WelcomeContribution implements BrowserEditorContribution, ClientAppContribution { 17 | @Autowired(IWorkspaceService) 18 | private readonly workspaceService: IWorkspaceService; 19 | 20 | @Autowired(WorkbenchEditorService) 21 | private readonly editorService: WorkbenchEditorService; 22 | 23 | @Autowired(RecentFilesManager) 24 | private readonly recentFilesManager: RecentFilesManager; 25 | 26 | registerEditorComponent(registry: EditorComponentRegistry) { 27 | registry.registerEditorComponent({ 28 | uid: 'welcome', 29 | scheme: 'welcome', 30 | component: EditorWelcomeComponent, 31 | renderMode: EditorComponentRenderMode.ONE_PER_WORKBENCH, 32 | }); 33 | registry.registerEditorComponentResolver('welcome', (resource, results) => { 34 | results.push({ 35 | type: EditorOpenType.component, 36 | componentId: 'welcome', 37 | }); 38 | }); 39 | } 40 | 41 | registerResource(service: ResourceService) { 42 | service.registerResourceProvider({ 43 | scheme: 'welcome', 44 | provideResource: async (uri: URI): Promise> => 45 | Promise.all([ 46 | this.workspaceService.getMostRecentlyUsedWorkspaces(), 47 | this.recentFilesManager.getMostRecentlyOpenedFiles(), 48 | ]).then(([workspaces, files]) => ({ 49 | uri, 50 | name: localize('welcome.title'), 51 | icon: '', 52 | metadata: { 53 | recentWorkspaces: workspaces || [], 54 | recentFiles: files || [], 55 | }, 56 | })), 57 | }); 58 | } 59 | 60 | onDidStart() { 61 | if (!this.workspaceService.workspace) { 62 | this.editorService.open(new URI('welcome://')); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/core/browser/welcome/welcome.module.less: -------------------------------------------------------------------------------- 1 | .welcome { 2 | h2 { 3 | color: var(--foreground); 4 | } 5 | padding: 20px 40px; 6 | > div { 7 | margin-bottom: 20px; 8 | } 9 | 10 | .recentRow { 11 | margin: 2px; 12 | line-height: 20px; 13 | .path { 14 | padding-left: 1em; 15 | opacity: 0.85; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/core/common/asar.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import module from 'node:module' 3 | 4 | function enableASARSupport() { 5 | const NODE_MODULES_PATH = path.join(__dirname, '../../node_modules'); 6 | const NODE_MODULES_ASAR_PATH = `${NODE_MODULES_PATH}.asar`; 7 | 8 | const Module = module.Module as any; 9 | const originalResolveLookupPaths = Module._resolveLookupPaths; 10 | Module._resolveLookupPaths = function (request: any, parent: any) { 11 | const paths = originalResolveLookupPaths(request, parent); 12 | if (Array.isArray(paths)) { 13 | for (let i = 0, len = paths.length; i < len; i++) { 14 | if (paths[i] === NODE_MODULES_PATH) { 15 | paths.splice(i, 0, NODE_MODULES_ASAR_PATH); 16 | break; 17 | } 18 | } 19 | } 20 | return paths; 21 | }; 22 | } 23 | 24 | enableASARSupport() 25 | -------------------------------------------------------------------------------- /src/core/common/constants.ts: -------------------------------------------------------------------------------- 1 | export const StorageKey = { 2 | THEME_BG_COLOR: 'themeBackgroundColor', 3 | } 4 | -------------------------------------------------------------------------------- /src/core/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants' 2 | export * from './types' -------------------------------------------------------------------------------- /src/core/common/types.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeType } from '@opensumi/ide-theme'; 2 | 3 | export { ThemeType } 4 | 5 | export interface ThemeData { 6 | menuBarBackground?: string; 7 | sideBarBackground?: string; 8 | editorBackground?: string; 9 | panelBackground?: string; 10 | statusBarBackground?: string; 11 | } 12 | 13 | export const IStorageService = 'IStorageService'; 14 | 15 | export type IStorageData = object | string | number | boolean | undefined | null; 16 | export interface IStorageService { 17 | getItem(key: string, defaultValue: T): T; 18 | getItem(key: string, defaultValue?: T): T | undefined; 19 | setItem(key: string, data?: IStorageData): void; 20 | setItems(items: readonly { key: string; data?: IStorageData }[]): void; 21 | removeItem(key: string): void; 22 | close(): Promise; 23 | } 24 | 25 | export const IThemeService = 'IThemeService'; 26 | 27 | export interface ThemeData { 28 | menuBarBackground?: string; 29 | sideBarBackground?: string; 30 | editorBackground?: string; 31 | panelBackground?: string; 32 | statusBarBackground?: string; 33 | themeType?: ThemeType; 34 | } 35 | 36 | export interface IThemeService { 37 | setTheme(windowId: number, themeData: ThemeData): void; 38 | } 39 | 40 | export const IAppMenuService = 'IAppMenuService'; 41 | 42 | export interface IAppMenuService { 43 | renderRecentWorkspaces(workspaces: string[]): Promise; 44 | } 45 | 46 | export const IProduct = Symbol('IProduct'); 47 | export interface IProduct { 48 | productName: string; 49 | applicationName: string; 50 | autoUpdaterConfigUrl: string; 51 | dataFolderName: string; 52 | commit: string, 53 | date: string, 54 | } 55 | 56 | export const IEnvironmentService = Symbol('IEnvironmentService'); 57 | export interface IEnvironmentService { 58 | isDev: boolean; 59 | dataFolderName: string; 60 | appRoot: string; 61 | userHome: string; 62 | userDataPath: string; 63 | userSettingPath: string; 64 | storagePath: string; 65 | extensionsPath: string; 66 | logRoot: string; 67 | logHome: string; 68 | } 69 | -------------------------------------------------------------------------------- /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/core/electron-main/environment.service.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | import * as os from 'node:os' 3 | import * as path from 'node:path' 4 | import { Injectable, Autowired } from '@opensumi/di' 5 | import { memoize } from '@opensumi/ide-core-common' 6 | import { IEnvironmentService, IProduct } from '../common' 7 | 8 | @Injectable() 9 | export class EnvironmentService implements IEnvironmentService { 10 | @Autowired(IProduct) 11 | product: IProduct 12 | 13 | @memoize 14 | get isDev() { return process.env.NODE_ENV === 'development' } 15 | 16 | @memoize 17 | get dataFolderName() { return this.product.dataFolderName } 18 | 19 | @memoize 20 | get appRoot(): string { return app.getAppPath() } 21 | 22 | @memoize 23 | get userHome() { return os.homedir() } 24 | 25 | @memoize 26 | get userDataPath(): string { return app.getPath('userData') } 27 | 28 | @memoize 29 | get userSettingPath(): string { return path.join(this.userDataPath, 'user') } 30 | 31 | @memoize 32 | get storagePath(): string { return path.join(this.userSettingPath, 'storage.json') } 33 | 34 | @memoize 35 | get extensionsPath() { 36 | return path.join(this.userHome, this.product.dataFolderName, 'extensions') 37 | } 38 | 39 | @memoize 40 | get logRoot() { 41 | return path.join(this.userDataPath, 'logs') 42 | } 43 | 44 | @memoize 45 | get logHome() { 46 | const date = new Date(); 47 | const logName = `${date.getFullYear()}${String((date.getMonth() + 1)).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}`; 48 | return path.join(this.logRoot, logName) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/core/electron-main/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app' 2 | export * from './module' 3 | export * from './types' -------------------------------------------------------------------------------- /src/core/electron-main/lifecycle.contribution.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | import * as fs from 'node:fs/promises' 3 | import { Autowired } from '@opensumi/di' 4 | import { Domain } from '@opensumi/ide-core-common' 5 | import { ILogService } from '@/logger/common' 6 | import { ElectronMainContribution } from './types' 7 | import { IEnvironmentService } from '../common' 8 | import { StorageService } from './storage.service' 9 | import { WindowsManager } from './window/windows-manager' 10 | 11 | @Domain(ElectronMainContribution) 12 | export class LifecycleContribution implements ElectronMainContribution { 13 | @Autowired(IEnvironmentService) 14 | environmentService: IEnvironmentService 15 | 16 | @Autowired(StorageService) 17 | storageService: StorageService; 18 | 19 | @Autowired(WindowsManager) 20 | windowsManager: WindowsManager 21 | 22 | @Autowired(ILogService) 23 | logger: ILogService 24 | 25 | async onWillStart() { 26 | this.setProcessEnv(); 27 | await Promise.all([ 28 | Promise.all([ 29 | this.environmentService.logHome, 30 | this.environmentService.extensionsPath, 31 | ].map(filepath => filepath ? fs.mkdir(filepath, { recursive: true }) : null)), 32 | this.storageService.init(), 33 | ]) 34 | } 35 | 36 | onStart() { 37 | this.windowsManager.createCodeWindow() 38 | 39 | app.on('activate', (_e, hasVisibleWindows) => { 40 | this.logger.debug('lifecycle#activate') 41 | if (!hasVisibleWindows) { 42 | this.windowsManager.createCodeWindow() 43 | } 44 | }) 45 | } 46 | 47 | private setProcessEnv() { 48 | const { dataFolderName, logRoot, logHome, extensionsPath } = this.environmentService; 49 | process.env.IDE_VERSION = app.getVersion(); 50 | process.env.IDE_DATA_FOLDER_NAME = dataFolderName; 51 | process.env.IDE_LOG_ROOT = logRoot 52 | process.env.IDE_LOG_HOME = logHome 53 | process.env.IDE_EXTENSIONS_PATH = extensionsPath 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/core/electron-main/menu.contribution.ts: -------------------------------------------------------------------------------- 1 | import { app, Menu, MenuItem } from 'electron' 2 | import { Autowired, Injectable } from '@opensumi/di' 3 | import { Domain, isMacintosh, localize, FileUri } from '@opensumi/ide-core-common' 4 | import { 5 | ElectronMainApiRegistry, 6 | ElectronMainApiProvider, 7 | } from '@opensumi/ide-core-electron-main/lib/bootstrap/types'; 8 | import { IAppMenuService } from '../common' 9 | import { ElectronMainContribution } from './types' 10 | import { WindowsManager } from './window/windows-manager' 11 | 12 | @Injectable() 13 | export class AppMenuService extends ElectronMainApiProvider implements IAppMenuService { 14 | async renderRecentWorkspaces(recentWorkspaces: string[]): Promise { 15 | const workspaces = recentWorkspaces.slice(0, 7) 16 | if (isMacintosh) { 17 | this.updateMacOSRecentDocuments(workspaces) 18 | } 19 | } 20 | 21 | private async updateMacOSRecentDocuments(workspaces: string[]): Promise { 22 | app.clearRecentDocuments(); 23 | workspaces.forEach(workspace => { 24 | let workspacePath = workspace; 25 | if (workspace.startsWith('file://')) { 26 | workspacePath = FileUri.fsPath(workspace); 27 | } 28 | app.addRecentDocument(workspacePath) 29 | }); 30 | } 31 | } 32 | 33 | @Domain(ElectronMainContribution) 34 | export class AppMenuContribution implements ElectronMainContribution { 35 | @Autowired(AppMenuService) 36 | menuService: AppMenuService; 37 | 38 | @Autowired(WindowsManager) 39 | windowsManager: WindowsManager 40 | 41 | #appMenuInstalled = false; 42 | 43 | registerMainApi(registry: ElectronMainApiRegistry) { 44 | registry.registerMainApi(IAppMenuService, this.menuService); 45 | } 46 | 47 | onStart(): void { 48 | this.installMenu() 49 | } 50 | 51 | installMenu() { 52 | if (isMacintosh && !this.#appMenuInstalled) { 53 | this.#appMenuInstalled = true; 54 | 55 | const dockMenu = new Menu(); 56 | dockMenu.append(new MenuItem({ label: localize('common.newWindow'), click: () => this.windowsManager.createCodeWindow() })); 57 | app.dock.setMenu(dockMenu); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/core/electron-main/module.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@opensumi/di'; 2 | import { ElectronMainModule } from '@opensumi/ide-core-electron-main/lib/electron-main-module'; 3 | import { StorageContribution, StorageService } from './storage.service'; 4 | import { ThemeContribution, ThemeService } from './theme.service'; 5 | import { LifecycleContribution } from './lifecycle.contribution' 6 | import { IProduct, IEnvironmentService } from '../common' 7 | import { EnvironmentService } from './environment.service' 8 | import { WindowsManager } from './window/windows-manager' 9 | import { AppMenuContribution, AppMenuService } from './menu.contribution' 10 | import { WindowContribution } from './window/window.contribution' 11 | import { WorkspaceHistoryContribution } from './workspace/workspace-history.contribution' 12 | 13 | export * from './storage.service' 14 | 15 | @Injectable() 16 | export class CoreElectronMainModule extends ElectronMainModule { 17 | providers = [ 18 | LifecycleContribution, 19 | StorageContribution, 20 | StorageService, 21 | ThemeContribution, 22 | ThemeService, 23 | AppMenuContribution, 24 | AppMenuService, 25 | WindowContribution, 26 | WindowsManager, 27 | WorkspaceHistoryContribution, 28 | { 29 | token: IProduct, 30 | useValue: __PRODUCT__, 31 | }, 32 | { 33 | token: IEnvironmentService, 34 | useClass: EnvironmentService, 35 | }, 36 | ]; 37 | } 38 | -------------------------------------------------------------------------------- /src/core/electron-main/storage.service.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import * as fs from 'node:fs/promises'; 3 | import { Injectable, Autowired } from '@opensumi/di'; 4 | import { Domain, isUndefinedOrNull, isUndefined, ThrottledDelayer, IDisposable } from '@opensumi/ide-core-common'; 5 | import { 6 | ElectronMainApiRegistry, 7 | ElectronMainContribution, 8 | ElectronMainApiProvider, 9 | } from '@opensumi/ide-core-electron-main/lib/bootstrap/types'; 10 | 11 | import { ILogService } from '@/logger/common'; 12 | import { IStorageService, IEnvironmentService, IStorageData } from '../common/types'; 13 | 14 | @Injectable() 15 | export class StorageService extends ElectronMainApiProvider implements IStorageService, IDisposable { 16 | #cache: Record = Object.create(null); 17 | #lastContents = ''; 18 | #initializing: Promise | undefined = undefined; 19 | #closing: Promise | undefined = undefined; 20 | readonly #flushDelayer = new ThrottledDelayer(100); 21 | 22 | @Autowired(IEnvironmentService) 23 | environmentService: IEnvironmentService; 24 | 25 | @Autowired(ILogService) 26 | logService: ILogService 27 | 28 | init(): Promise { 29 | if (!this.#initializing) { 30 | this.#initializing = this.doInit(); 31 | } 32 | 33 | return this.#initializing; 34 | } 35 | 36 | private async doInit() { 37 | try { 38 | await fs.mkdir(path.dirname(this.environmentService.storagePath), { recursive: true }); 39 | this.#lastContents = await fs.readFile(this.environmentService.storagePath, 'utf8'); 40 | this.#cache = JSON.parse(this.#lastContents); 41 | } catch (error: any) { 42 | if (error.code !== 'ENOENT') { 43 | this.logService.error(error); 44 | } 45 | } 46 | } 47 | 48 | getItem(key: string, defaultValue: T): T; 49 | getItem(key: string, defaultValue?: T): T | undefined; 50 | getItem(key: string, defaultValue?: T): T | undefined { 51 | const res = this.#cache[key]; 52 | if (isUndefinedOrNull(res)) { 53 | return defaultValue; 54 | } 55 | 56 | return res as T; 57 | } 58 | 59 | setItem(key: string, data?: IStorageData): void { 60 | this.setItems([{ key, data }]); 61 | } 62 | 63 | setItems(items: readonly { key: string; data?: IStorageData }[]): void { 64 | let needSave = false; 65 | 66 | for (const { key, data } of items) { 67 | if (this.#cache[key] === data) continue; 68 | if (isUndefinedOrNull(data) && isUndefined(this.#cache[key])) continue; 69 | this.#cache[key] = isUndefinedOrNull(data) ? undefined : data; 70 | needSave = true 71 | } 72 | 73 | if (needSave) { 74 | this.save(); 75 | } 76 | } 77 | 78 | removeItem(key: string): void { 79 | if (!isUndefined(this.#cache[key])) { 80 | this.#cache[key] = undefined; 81 | this.save(); 82 | } 83 | } 84 | 85 | private async save(): Promise { 86 | if (this.#closing) { 87 | return; 88 | } 89 | 90 | return this.#flushDelayer.trigger(() => this.doSave()); 91 | } 92 | 93 | private async doSave(): Promise { 94 | if (!this.#initializing) { 95 | return; 96 | } 97 | await this.#initializing; 98 | 99 | const serializedContent = JSON.stringify(this.#cache, null, 4); 100 | if (serializedContent === this.#lastContents) { 101 | return; 102 | } 103 | 104 | try { 105 | await fs.writeFile(this.environmentService.storagePath, serializedContent); 106 | this.#lastContents = serializedContent; 107 | } catch (error) { 108 | this.logService.error(error); 109 | } 110 | } 111 | 112 | async close(): Promise { 113 | if (!this.#closing) { 114 | this.#closing = this.#flushDelayer.trigger(() => this.doSave(), 0); 115 | } 116 | 117 | return this.#closing; 118 | } 119 | 120 | dispose(): void { 121 | this.#flushDelayer.dispose(); 122 | } 123 | } 124 | 125 | @Domain(ElectronMainContribution) 126 | export class StorageContribution implements ElectronMainContribution { 127 | @Autowired(StorageService) 128 | storageService: StorageService; 129 | 130 | registerMainApi(registry: ElectronMainApiRegistry) { 131 | registry.registerMainApi(IStorageService, this.storageService); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/core/electron-main/theme.service.ts: -------------------------------------------------------------------------------- 1 | import { nativeTheme } from 'electron'; 2 | import { Injectable, Autowired } from '@opensumi/di'; 3 | import { Domain, isWindows } from '@opensumi/ide-core-common'; 4 | import { 5 | ElectronMainApiRegistry, 6 | ElectronMainContribution, 7 | ElectronMainApiProvider, 8 | } from '@opensumi/ide-core-electron-main/lib/bootstrap/types'; 9 | import { IElectronMainApp } from '@opensumi/ide-core-electron-main' 10 | import { Color } from '@opensumi/ide-theme/lib/common/color'; 11 | 12 | import { StorageService } from './storage.service' 13 | import { IThemeService, ThemeData, ThemeType } from '../common/types'; 14 | import { StorageKey } from '../common'; 15 | 16 | @Injectable() 17 | export class ThemeService extends ElectronMainApiProvider implements IThemeService { 18 | @Autowired(StorageService) 19 | storageService: StorageService 20 | 21 | @Autowired(IElectronMainApp) 22 | electronMainApp: IElectronMainApp 23 | 24 | setTheme(windowId: number, themeData: ThemeData): void { 25 | this.storageService.setItem(StorageKey.THEME_BG_COLOR, themeData) 26 | this.updateSystemColorTheme(themeData.themeType); 27 | if (themeData.menuBarBackground && isWindows) { 28 | const currentWindow = this.electronMainApp.getCodeWindows().find(codeWindow => codeWindow.getBrowserWindow().id === windowId) 29 | if (currentWindow) { 30 | currentWindow.getBrowserWindow().setTitleBarOverlay(this.getTitleBarOverlay(themeData.menuBarBackground!)) 31 | } 32 | } 33 | } 34 | 35 | getTitleBarOverlay(color: string) { 36 | return { 37 | height: 35, 38 | color, 39 | symbolColor: Color.fromHex(color).isDarker() ? '#FFFFFF' : '#000000', 40 | } 41 | } 42 | 43 | get themeBackgroundColor() { 44 | return this.storageService.getItem(StorageKey.THEME_BG_COLOR, {}) 45 | } 46 | 47 | setSystemTheme() { 48 | const theme = this.storageService.getItem(StorageKey.THEME_BG_COLOR) 49 | this.updateSystemColorTheme(theme?.themeType || 'dark'); 50 | } 51 | 52 | private updateSystemColorTheme(themeType?: ThemeType) { 53 | switch (themeType) { 54 | case 'light': nativeTheme.themeSource = 'light'; break; 55 | case 'dark': nativeTheme.themeSource = 'dark'; break; 56 | default: nativeTheme.themeSource = 'system'; 57 | } 58 | } 59 | } 60 | 61 | @Domain(ElectronMainContribution) 62 | export class ThemeContribution implements ElectronMainContribution { 63 | @Autowired(ThemeService) 64 | themeService: ThemeService; 65 | 66 | registerMainApi(registry: ElectronMainApiRegistry) { 67 | registry.registerMainApi(IThemeService, this.themeService); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/core/electron-main/types.ts: -------------------------------------------------------------------------------- 1 | import { MaybePromise, } from '@opensumi/ide-core-common' 2 | import { ElectronMainContribution as BaseElectronMainContribution } from '@opensumi/ide-core-electron-main'; 3 | 4 | export const ElectronMainContribution = BaseElectronMainContribution; 5 | 6 | export interface ElectronMainContribution extends BaseElectronMainContribution { 7 | /** 8 | * app.isReady 之前 9 | */ 10 | onBeforeReady?(): void; 11 | /** 12 | * after app.isReady 13 | */ 14 | onWillStart?(): MaybePromise; 15 | 16 | /** 17 | * after all onWillStart 18 | */ 19 | onStart?(): MaybePromise; 20 | 21 | /** 22 | * app event will-quit 23 | */ 24 | onWillQuit?(): MaybePromise; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/core/electron-main/window/window-lifecycle.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron'; 2 | import { Injector } from '@opensumi/di' 3 | import { IElectronMainApiProvider, ElectronMainApp, IWindowOpenOptions } from '@opensumi/ide-core-electron-main' 4 | import { ExtensionCandidate, URI } from '@opensumi/ide-core-common'; 5 | import { WindowsManager } from './windows-manager' 6 | 7 | export class WindowLifecycle implements IElectronMainApiProvider { 8 | eventEmitter: undefined; 9 | 10 | constructor(private app: ElectronMainApp, private injector: Injector) {} 11 | 12 | openWorkspace(workspace: string, openOptions: IWindowOpenOptions) { 13 | this.injector.get(WindowsManager).openCodeWindow(URI.parse(workspace), openOptions); 14 | } 15 | 16 | minimizeWindow(windowId: number) { 17 | const window = BrowserWindow.fromId(windowId); 18 | if (window) { 19 | window.minimize(); 20 | } 21 | } 22 | 23 | fullscreenWindow(windowId: number) { 24 | const window = BrowserWindow.fromId(windowId); 25 | if (window) { 26 | window.setFullScreen(true); 27 | } 28 | } 29 | maximizeWindow(windowId: number) { 30 | const window = BrowserWindow.fromId(windowId); 31 | if (window) { 32 | window.maximize(); 33 | } 34 | } 35 | 36 | unmaximizeWindow(windowId: number) { 37 | const window = BrowserWindow.fromId(windowId); 38 | if (window) { 39 | window.unmaximize(); 40 | } 41 | } 42 | closeWindow(windowId: number) { 43 | const window = BrowserWindow.fromId(windowId); 44 | if (window) { 45 | const codeWindow = this.app.getCodeWindowByElectronBrowserWindowId(windowId); 46 | if (!codeWindow) { 47 | window.close(); 48 | return; 49 | } 50 | 51 | if (codeWindow.isReloading) { 52 | codeWindow.isReloading = false; 53 | 54 | if (!codeWindow.isRemote) { 55 | // reload 的情况下不需要等待 startNode 执行完 56 | // 所以可以同时执行 startNode 和 reload 前端 57 | codeWindow.startNode(); 58 | } 59 | window.webContents.reload(); 60 | } else { 61 | // 正常关闭窗口的情况下,需要回收子进程,耗时可能会比较长 62 | // 这里先隐藏窗口,体感会更快 63 | window.hide(); 64 | codeWindow.clear().finally(() => { 65 | window.close(); 66 | }); 67 | } 68 | } 69 | } 70 | 71 | reloadWindow(windowId: number) { 72 | const codeWindow = this.app.getCodeWindowByElectronBrowserWindowId(windowId); 73 | if (codeWindow) { 74 | codeWindow.reload(); 75 | } 76 | } 77 | 78 | setExtensionDir(extensionDir: string, windowId: number) { 79 | const window = BrowserWindow.fromId(windowId); 80 | if (window) { 81 | const codeWindow = this.app.getCodeWindowByElectronBrowserWindowId(windowId); 82 | if (codeWindow) { 83 | codeWindow.setExtensionDir(extensionDir); 84 | } 85 | } 86 | } 87 | 88 | setExtensionCandidate(candidate: ExtensionCandidate[], windowId: number) { 89 | const window = BrowserWindow.fromId(windowId); 90 | if (window) { 91 | const codeWindow = this.app.getCodeWindowByElectronBrowserWindowId(windowId); 92 | if (codeWindow) { 93 | codeWindow.setExtensionCandidate(candidate); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/core/electron-main/window/window.contribution.ts: -------------------------------------------------------------------------------- 1 | import { Injector, Autowired, INJECTOR_TOKEN } from '@opensumi/di' 2 | import { Domain } from '@opensumi/ide-core-common' 3 | import { 4 | ElectronMainApiRegistry, 5 | ElectronMainContribution, 6 | IElectronMainApp, 7 | } from '@opensumi/ide-core-electron-main/lib/bootstrap/types'; 8 | import { ElectronMainApp } from '@opensumi/ide-core-electron-main'; 9 | import { IElectronMainLifeCycleService } from '@opensumi/ide-core-common/lib/electron'; 10 | import { WindowLifecycle } from './window-lifecycle' 11 | 12 | @Domain(ElectronMainContribution) 13 | export class WindowContribution implements ElectronMainContribution { 14 | @Autowired(INJECTOR_TOKEN) 15 | injector: Injector 16 | 17 | @Autowired(IElectronMainApp) 18 | electronApp: ElectronMainApp; 19 | 20 | registerMainApi(registry: ElectronMainApiRegistry): void { 21 | registry.registerMainApi(IElectronMainLifeCycleService, new WindowLifecycle(this.electronApp, this.injector)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/core/electron-main/window/windows-manager.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindowConstructorOptions } from 'electron' 2 | import { isWindows, URI } from '@opensumi/ide-core-common'; 3 | import { Injectable, INJECTOR_TOKEN, Autowired, Injector } from '@opensumi/di' 4 | import { IWindowOpenOptions, ElectronAppConfig, IElectronMainApp, ElectronMainApp } from '@opensumi/ide-core-electron-main' 5 | import { IEnvironmentService, StorageKey } from '../../common' 6 | import { ThemeService } from '../theme.service' 7 | 8 | @Injectable() 9 | export class WindowsManager { 10 | @Autowired(INJECTOR_TOKEN) 11 | injector: Injector 12 | 13 | @Autowired(ElectronAppConfig) 14 | appConfig: ElectronAppConfig 15 | 16 | @Autowired(IElectronMainApp) 17 | mainApp: ElectronMainApp 18 | 19 | @Autowired(IEnvironmentService) 20 | environmentService: IEnvironmentService 21 | 22 | @Autowired(ThemeService) 23 | themeService: ThemeService; 24 | 25 | openCodeWindow(workspaceUri?: URI, options?: IWindowOpenOptions) { 26 | if (workspaceUri) { 27 | for (const codeWindow of this.mainApp.getCodeWindows()) { 28 | if (codeWindow.workspace?.toString() === workspaceUri.toString()) { 29 | codeWindow.getBrowserWindow().show() 30 | return; 31 | } 32 | } 33 | } 34 | if (options?.windowId) { 35 | const codeWindow = this.mainApp.getCodeWindowByElectronBrowserWindowId(options.windowId) 36 | if (codeWindow) { 37 | if (workspaceUri) { 38 | codeWindow.setWorkspace(workspaceUri.toString()); 39 | } 40 | codeWindow.reload(); 41 | return; 42 | } 43 | } 44 | this.createCodeWindow(workspaceUri); 45 | } 46 | 47 | createCodeWindow( 48 | workspaceUri?: URI, 49 | metadata?: any, 50 | browserWindowOptions?: BrowserWindowConstructorOptions, 51 | ) { 52 | this.themeService.setSystemTheme(); 53 | const editorBackground = this.themeService.themeBackgroundColor.editorBackground || '#1e1e1e' 54 | const menuBarBackground = this.themeService.themeBackgroundColor.menuBarBackground || editorBackground; 55 | 56 | const codeWindow = this.mainApp.loadWorkspace( 57 | workspaceUri ? workspaceUri.toString() : undefined, 58 | { 59 | ...metadata, 60 | environment: { 61 | dataFolderName: this.environmentService.dataFolderName, 62 | isDev: this.environmentService.isDev, 63 | logRoot: this.environmentService.logRoot, 64 | }, 65 | }, 66 | { 67 | trafficLightPosition: { 68 | x: 10, 69 | y: 10, 70 | }, 71 | ...(isWindows ? { 72 | titleBarOverlay: this.themeService.getTitleBarOverlay(menuBarBackground) 73 | } : null), 74 | show: false, 75 | backgroundColor: editorBackground, 76 | ...browserWindowOptions, 77 | webPreferences: { 78 | preload: this.appConfig.browserPreload, 79 | nodeIntegration: this.appConfig.browserNodeIntegrated, 80 | webviewTag: true, 81 | contextIsolation: false, 82 | webSecurity: !this.environmentService.isDev, 83 | }, 84 | } 85 | ); 86 | 87 | const browserWindow = codeWindow.getBrowserWindow() 88 | // 默认全屏 89 | // TODO: 支持窗口状态缓存 90 | browserWindow.maximize(); 91 | browserWindow.show(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/core/electron-main/workspace/workspace-history.contribution.ts: -------------------------------------------------------------------------------- 1 | import { Autowired } from '@opensumi/di' 2 | import { app, JumpListCategory } from 'electron' 3 | import { Domain, isWindows, localize, MaybePromise } from '@opensumi/ide-core-common' 4 | import { ILogService } from '@/logger/common' 5 | import { ElectronMainContribution } from '../types' 6 | 7 | @Domain(ElectronMainContribution) 8 | export class WorkspaceHistoryContribution implements ElectronMainContribution { 9 | @Autowired(ILogService) 10 | logger: ILogService 11 | 12 | onWillStart(): MaybePromise { 13 | this.handleWindowsJumpList() 14 | } 15 | 16 | private async handleWindowsJumpList(): Promise { 17 | if (!isWindows) { 18 | return; 19 | } 20 | await this.updateWindowsJumpList(); 21 | } 22 | 23 | private async updateWindowsJumpList(): Promise { 24 | const jumpList: JumpListCategory[] = []; 25 | jumpList.push({ 26 | type: 'tasks', 27 | items: [ 28 | { 29 | type: 'task', 30 | title: localize('common.newWindow'), 31 | description: localize('common.newWindowDesc'), 32 | program: process.execPath, 33 | iconPath: process.execPath, 34 | iconIndex: 0 35 | } 36 | ] 37 | }); 38 | 39 | try { 40 | const res = app.setJumpList(jumpList); 41 | if (res && res !== 'ok') { 42 | this.logger.warn(`updateWindowsJumpList#setJumpList unexpected result: ${res}`); 43 | } 44 | } catch (error) { 45 | this.logger.warn('updateWindowsJumpList#setJumpList', error); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/core/node/index.ts: -------------------------------------------------------------------------------- 1 | import { NodeModule } from '@opensumi/ide-core-node'; 2 | import { Injectable } from '@opensumi/di'; 3 | 4 | @Injectable() 5 | export class CoreNodeModule extends NodeModule { 6 | providers = []; 7 | } 8 | -------------------------------------------------------------------------------- /src/i18n/en-US.ts: -------------------------------------------------------------------------------- 1 | export const localizationBundle = { 2 | languageId: 'en-US', 3 | languageName: 'English', 4 | localizedLanguageName: 'English', 5 | contents: { 6 | 'common.about': 'About', 7 | 'common.preferences': 'Preferences', 8 | 'common.newWindow': 'New Window', 9 | 'common.newWindowDesc': 'Open a new window', 10 | 11 | 'custom.quick_open': 'Quick Open', 12 | 'custom.command_palette': 'Command Palette', 13 | 'custom.terminal_panel': 'Switch to Terminal Panel', 14 | 'custom.search_panel': 'Switch to Search Panel', 15 | 16 | 'preference.ai.model.title': 'Completion Model', 17 | 'preference.ai.model.baseUrl': 'Base URL', 18 | 'preference.ai.model.api_key': 'API Key', 19 | 'preference.ai.model.code': 'Code > Completion', 20 | 'preference.ai.model.code.modelName': 'Code > Model Name', 21 | 'preference.ai.model.code.systemPrompt': 'Code > System Prompt', 22 | 'preference.ai.model.code.temperature': 'Code > temperature', 23 | 'preference.ai.model.code.maxTokens': 'Code > max_tokens', 24 | 'preference.ai.model.code.presencePenalty': 'Code > presence_penalty', 25 | 'preference.ai.model.code.frequencyPenalty': 'Code > frequency_penalty', 26 | 'preference.ai.model.code.topP': 'Code Completion > top_p', 27 | 'preference.ai.model.code.modelName.tooltip': 'The default is same as Chat Model Name', 28 | 'preference.ai.model.code.fimTemplate': 'Code > FIM Template', 29 | 'preference.ai.model.code.fimTemplate.tooltip': 'If no template is provided, the pre-cursor and post-cursor code will be sent directly to the api, and if a template is provided, the following format should be configured\n{prefix}{suffix}\n{prefix} will be replaced with the pre-cursor code, and {suffix} will be replaced with the post-cursor code', 30 | 'preference.ai.model.temperature.description': 'What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.\nWe generally recommend altering this or top_p but not both.', 31 | 'preference.ai.model.maxTokens.description': 'The maximum number of tokens that can be generated in the chat completion.', 32 | 'preference.ai.model.presencePenalty.description': 'Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model\'s likelihood to talk about new topics.', 33 | 'preference.ai.model.frequencyPenalty.description': 'Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model\'s likelihood to repeat the same line verbatim.', 34 | 'preference.ai.model.topP.description': 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\nWe generally recommend altering this or temperature but not both.', 35 | 36 | 'ai.model.noConfig': 'Please configure the AI model service for a better experience', 37 | 'ai.model.go': 'Go', 38 | 39 | 'autoUpdater.checkForUpdates': 'Check for Updates...', 40 | 'codefuse-ide.openLogDir': 'Open Log Folder', 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { registerLocalizationBundle } from '@opensumi/ide-core-common/lib/localize'; 2 | import { localizationBundle as zh } from './zh-CN'; 3 | import { localizationBundle as en } from './en-US'; 4 | 5 | // 先初始化语言包 6 | registerLocalizationBundle(zh); 7 | registerLocalizationBundle(en); 8 | -------------------------------------------------------------------------------- /src/i18n/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export const localizationBundle = { 2 | languageId: 'zh-CN', 3 | languageName: 'Chinese', 4 | localizedLanguageName: '中文(中国)', 5 | contents: { 6 | 'common.about': '关于', 7 | 'common.preferences': '首选项', 8 | 'common.newWindow': '新建窗口', 9 | 'common.newWindowDesc': '打开新的窗口', 10 | 11 | 'custom.quick_open': '转到文件', 12 | 'custom.command_palette': '显示所有命令', 13 | 'custom.terminal_panel': '切换终端', 14 | 'custom.search_panel': '切换搜索面板', 15 | 16 | 'preference.ai.model.title': '补全模型配置', 17 | 'preference.ai.model.baseUrl': 'API URL 前缀', 18 | 'preference.ai.model.apiKey': 'API Key', 19 | 'preference.ai.model.code': '代码 > 补全', 20 | 'preference.ai.model.code.modelName': '代码 > 模型名称', 21 | 'preference.ai.model.code.systemPrompt': '代码 > 系统提示词', 22 | 'preference.ai.model.code.temperature': '代码 > temperature', 23 | 'preference.ai.model.code.maxTokens': '代码 > max_tokens', 24 | 'preference.ai.model.code.presencePenalty': '代码 > presence_penalty', 25 | 'preference.ai.model.code.frequencyPenalty': '代码 > frequency_penalty', 26 | 'preference.ai.model.code.topP': '代码 > top_p', 27 | 'preference.ai.model.code.modelName.tooltip': '默认和对话模型一致', 28 | 'preference.ai.model.code.fimTemplate': 'FIM 模版', 29 | 'preference.ai.model.code.fimTemplate.tooltip': '如果未提供模版, 则将光标前后代码直接发送到接口, 如果提供了模版, 配置如下格式:“{prefix}{suffix}”,{prefix} 会替换为光标前代码,{suffix} 会替换为光标后代码', 30 | 'preference.ai.model.temperature.description': '采样温度,介于 0 和 2 之间。较高的值(如 0.8)将使输出更加随机,而较低的值(如 0.2)将使其更加集中性和确定性。\n通常建议只改变 top_p 或 temperature,不要两个都改', 31 | 'preference.ai.model.maxTokens.description': '补全完成时可以生成的最大 token 数。', 32 | 'preference.ai.model.presencePenalty.description': '存在惩罚,介于 -2.0 和 2.0 之间的数字。正值会根据新生成的词汇是否出现在目前的文本中来进行惩罚,从而增加模型讨论新话题的可能性。', 33 | 'preference.ai.model.frequencyPenalty.description': '频率惩罚,介于 -2.0 和 2.0 之间的数字。正值根据新标记到目前为止在文本中的现有频率对其进行惩罚,从而降低了模型逐字重复同一行的可能性。', 34 | 'preference.ai.model.topP.description': '温度采样的一种替代方法,称为原子核抽样,模型只会考虑前 top_p 概率质量的标记结果。因此,0.1 表示仅考虑前 10% 概率质量的标记。\n通常建议只改变 top_p 或 temperature,不要两个都改', 35 | 36 | 'ai.model.noConfig': '为了更好的体验,请先配置 AI 模型服务', 37 | 'ai.model.go': '前往', 38 | 39 | 'autoUpdater.checkForUpdates': '检查更新', 40 | 'codefuse-ide.openLogDir': '打开日志文件夹', 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/logger/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from './log-manager' 2 | export * from './log-service' 3 | export * from './types' -------------------------------------------------------------------------------- /src/logger/common/log-manager.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path' 2 | import { Emitter } from '@opensumi/ide-core-common'; 3 | import { ILogServiceManager, LogLevel, SupportLogNamespace, ILogService, BaseLogServiceOptions, Archive } from '@opensumi/ide-logs' 4 | import { SpdLogger } from './log-service' 5 | 6 | export abstract class AbstractLogServiceManager implements ILogServiceManager { 7 | #logLevel = process.env.NODE_ENV === 'development' ? LogLevel.Debug : LogLevel.Info; 8 | #logMap = new Map(); 9 | #logLevelChangeEmitter = new Emitter(); 10 | 11 | getLogger(namespace: SupportLogNamespace | string, loggerOptions?: BaseLogServiceOptions): ILogService { 12 | let logger = this.#logMap.get(namespace); 13 | if (logger) { 14 | if (loggerOptions) { 15 | logger.setOptions(loggerOptions) 16 | } 17 | return logger 18 | } 19 | // 默认使用 spdlog,上层也可拿到 logger 再次封装 20 | const options = { 21 | namespace, 22 | logLevel: this.getGlobalLogLevel(), 23 | logServiceManager: this, 24 | ...loggerOptions, 25 | } 26 | logger = new SpdLogger({ 27 | logLevel: options.logLevel, 28 | logPath: options.logDir || path.join(this.getLogFolder(), `${namespace}.log`), 29 | }); 30 | this.#logMap.set(namespace, logger); 31 | return logger; 32 | } 33 | 34 | removeLogger(namespace: SupportLogNamespace) { 35 | this.#logMap.delete(namespace); 36 | } 37 | 38 | getGlobalLogLevel(): LogLevel { 39 | return this.#logLevel; 40 | } 41 | 42 | setGlobalLogLevel(level: LogLevel) { 43 | this.#logLevel = level; 44 | } 45 | 46 | get onDidChangeLogLevel() { 47 | return this.#logLevelChangeEmitter.event; 48 | } 49 | 50 | abstract getLogFolder(): string 51 | 52 | abstract getRootLogFolder(): string 53 | 54 | async cleanOldLogs() {} 55 | 56 | async cleanAllLogs() {} 57 | 58 | async cleanExpiredLogs(_day: number) {} 59 | 60 | async getLogZipArchiveByDay(_day: number): Promise { return { pipe: () => null } } 61 | 62 | async getLogZipArchiveByFolder(_foldPath: string): Promise { return { pipe: () => null } } 63 | 64 | dispose() { 65 | this.#logLevelChangeEmitter.dispose(); 66 | this.#logMap.forEach((logger) => { 67 | logger.dispose(); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /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/logger/common/types.ts: -------------------------------------------------------------------------------- 1 | import type { ILogService as _ILogService } from '@opensumi/ide-logs/lib/common' 2 | export const ILogService = Symbol('ILogService'); 3 | export interface ILogService extends _ILogService { 4 | info(...args: any[]): void; 5 | } 6 | -------------------------------------------------------------------------------- /src/logger/electron-main/index.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Provider, Injector } from '@opensumi/di'; 2 | import { ElectronMainModule } from '@opensumi/ide-core-electron-main/lib/electron-main-module'; 3 | import { ILogServiceManager, SupportLogNamespace } from '@opensumi/ide-logs'; 4 | import { LogServiceManager } from './log-manager' 5 | import { ILogService } from '../common' 6 | 7 | @Injectable() 8 | export class LoggerModule extends ElectronMainModule { 9 | providers: Provider[] = [ 10 | { 11 | token: ILogServiceManager, 12 | useClass: LogServiceManager, 13 | }, 14 | { 15 | token: ILogService, 16 | useFactory: (injector: Injector) => { 17 | return (injector.get(ILogServiceManager)).getLogger(SupportLogNamespace.Main) 18 | } 19 | } 20 | ]; 21 | } 22 | -------------------------------------------------------------------------------- /src/logger/electron-main/log-manager.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises' 2 | import * as path from 'path' 3 | import { Injectable, Autowired } from '@opensumi/di'; 4 | import { IEnvironmentService } from '@/core/common' 5 | import { AbstractLogServiceManager } from '../common' 6 | 7 | @Injectable() 8 | export class LogServiceManager extends AbstractLogServiceManager { 9 | @Autowired(IEnvironmentService) 10 | environmentService: IEnvironmentService 11 | 12 | constructor() { 13 | super(); 14 | // 启动时清除旧日志,后续加个定时任务清除 15 | this.cleanOldLogs(); 16 | } 17 | 18 | getRootLogFolder(): string { 19 | return this.environmentService.logRoot; 20 | } 21 | 22 | getLogFolder(): string { 23 | return this.environmentService.logHome; 24 | } 25 | 26 | async cleanOldLogs(): Promise { 27 | try { 28 | const { logHome, logRoot } = this.environmentService; 29 | const currentLog = path.basename(logHome); 30 | const children = await fs.readdir(logRoot); 31 | const allSessions = children.filter((name) => /^\d{8}$/.test(name)); 32 | const oldSessions = allSessions.sort().filter((d) => d !== currentLog); 33 | const toDelete = oldSessions.slice(0, Math.max(0, oldSessions.length - 4)); 34 | if (toDelete.length > 0) { 35 | await Promise.all(toDelete.map((name) => fs.rm(path.join(logRoot, name), { recursive: true, force: true, maxRetries: 3 }))); 36 | } 37 | } catch { 38 | // noop 39 | } 40 | } 41 | 42 | async cleanAllLogs(): Promise { 43 | try { 44 | const { logRoot } = this.environmentService; 45 | const children = await fs.readdir(logRoot); 46 | const allSessions = children.filter((name) => /^\d{8}$/.test(name)); 47 | if (allSessions.length > 0) { 48 | await Promise.all(allSessions.map((name) => fs.rm(path.join(logRoot, name), { recursive: true, force: true, maxRetries: 3 }))); 49 | } 50 | } catch { 51 | // noop 52 | } 53 | } 54 | 55 | async cleanExpiredLogs(day: number): Promise { 56 | try { 57 | const { logRoot } = this.environmentService; 58 | const children = await fs.readdir(logRoot); 59 | const expiredSessions = children.filter((name) => /^\d{8}$/.test(name) && Number(name) < day); 60 | if (expiredSessions.length > 0) { 61 | await Promise.all(expiredSessions.map((name) => fs.rm(path.join(logRoot, name), { recursive: true, force: true, maxRetries: 3 }))); 62 | } 63 | } catch { 64 | // noop 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/logger/node/index.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Provider } from '@opensumi/di'; 2 | import { NodeModule } from '@opensumi/ide-core-node'; 3 | import { ILogServiceManager } from '@opensumi/ide-logs'; 4 | import { LogServiceManager } from './log-manager' 5 | 6 | @Injectable() 7 | export class LoggerModule extends NodeModule { 8 | providers: Provider[] = [ 9 | { 10 | token: ILogServiceManager, 11 | useClass: LogServiceManager, 12 | override: true, 13 | }, 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /src/logger/node/log-manager.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { Injectable } from '@opensumi/di'; 3 | import { AbstractLogServiceManager } from '../common' 4 | import * as process from "node:process"; 5 | 6 | @Injectable() 7 | export class LogServiceManager extends AbstractLogServiceManager { 8 | getRootLogFolder(): string { 9 | return process.env.IDE_LOG_ROOT!; 10 | } 11 | 12 | getLogFolder(): string { 13 | return path.join(process.env.IDE_LOG_HOME || '', `window${process.env.CODE_WINDOW_CLIENT_ID?.slice('CODE_WINDOW_CLIENT_ID:'?.length)}`) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2022", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "strictPropertyInitialization": false, 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "importHelpers": true, 12 | "resolveJsonModule": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "downlevelIteration": true, 16 | "noEmitOnError": false, 17 | "noImplicitAny": false, 18 | "skipLibCheck": true, 19 | "strictFunctionTypes": false, 20 | "jsx": "react", 21 | "baseUrl": ".", 22 | "rootDir": ".", 23 | "outDir": "dist", 24 | "noUnusedLocals": false, 25 | "allowSyntheticDefaultImports": true, 26 | "esModuleInterop": true, 27 | "declaration": true, 28 | "useDefineForClassFields": false, 29 | "lib": [ 30 | "DOM", 31 | "ES2022", 32 | ], 33 | "paths": { 34 | "@/*": ["./src/*"], 35 | }, 36 | "typeRoots": [ 37 | "./node_modules/@types", 38 | "./typings" 39 | ] 40 | }, 41 | "include": [ 42 | "src", 43 | "build", 44 | "typings", 45 | ], 46 | "exclude": [ 47 | "node_modules", 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /typings/global/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.less'; 2 | declare module '*.png'; 3 | declare module '*.svg'; 4 | 5 | declare const __PRODUCT__: any; 6 | declare const __CODE_WINDOW_DEV_SERVER_URL__: string; 7 | declare const __CODE_WINDOW_NAME__: string; 8 | declare const __UPDATE_WINDOW_NAME__: string; 9 | declare const __UPDATE_WINDOW_DEV_SERVER_URL__: string; 10 | --------------------------------------------------------------------------------