├── docs ├── .nvmrc ├── src │ ├── en │ │ ├── electron-how-to │ │ │ ├── index.md │ │ │ ├── preload-script.md │ │ │ └── main-and-renderer-process.md │ │ ├── project-structures │ │ │ ├── index.md │ │ │ └── specification-references.md │ │ ├── installation-and-build │ │ │ ├── index.md │ │ │ ├── install-local-documentation.md │ │ │ ├── automated-testing.md │ │ │ ├── npm-scripts.md │ │ │ ├── getting-started.md │ │ │ └── build-configuration.md │ │ ├── other-projects.md │ │ └── introduction.md │ ├── public │ │ ├── robots.txt │ │ ├── icon.png │ │ └── favicon.ico │ ├── zhHans │ │ ├── project-structures │ │ │ ├── index.md │ │ │ └── specification-references.md │ │ ├── electron-how-to │ │ │ ├── index.md │ │ │ ├── preload-script.md │ │ │ └── main-and-renderer-process.md │ │ ├── installation-and-build │ │ │ ├── index.md │ │ │ ├── install-local-documentation.md │ │ │ ├── automated-testing.md │ │ │ ├── getting-started.md │ │ │ ├── npm-scripts.md │ │ │ └── build-configuration.md │ │ ├── other-projects.md │ │ └── introduction.md │ └── .vitepress │ │ └── config.mts ├── .gitignore ├── tsconfig.json └── package.json ├── src ├── renderer │ ├── public │ │ ├── images │ │ │ └── .gitkeep │ │ └── flags │ │ │ ├── us.svg │ │ │ └── cn.svg │ ├── screens │ │ ├── ErrorScreen.vue │ │ ├── chat │ │ │ ├── ChatInputScreen.vue │ │ │ ├── ChatEndScreen.vue │ │ │ ├── ChatScreen.vue │ │ │ └── ChatHistoryScreen.vue │ │ ├── agent │ │ │ ├── AgentSideDock.vue │ │ │ └── AgentSideDrawer.vue │ │ ├── setting │ │ │ ├── SettingScreen.vue │ │ │ ├── SettingMenuScreen.vue │ │ │ └── SettingEndScreen.vue │ │ ├── mcp │ │ │ ├── McpCentralStage.vue │ │ │ └── McpSideDrawer.vue │ │ ├── index.ts │ │ └── PopupScreen.vue │ ├── components │ │ ├── layouts │ │ │ ├── FooterLayout.vue │ │ │ ├── index.ts │ │ │ ├── SidebarLayout.vue │ │ │ └── DefaultLayout.vue │ │ ├── common │ │ │ ├── LogoAvatar.vue │ │ │ ├── CommandCard.vue │ │ │ ├── ImgDialog.vue │ │ │ ├── MarkdownCard.vue │ │ │ ├── LocaleBtn.vue │ │ │ └── ConfigJsonCard.vue │ │ └── pages │ │ │ ├── McpConfigPage.vue │ │ │ ├── McpRegistryPage.vue │ │ │ ├── McpProcessPage.vue │ │ │ ├── McpEditPage.vue │ │ │ ├── McpNewsPage.vue │ │ │ └── McpDxtPage.vue │ ├── composables │ │ ├── useRules.ts │ │ └── useRouteFeatures.ts │ ├── utils │ │ └── color.ts │ ├── types │ │ ├── session.ts │ │ ├── registry.ts │ │ └── index.ts │ ├── store │ │ ├── counter.ts │ │ ├── layout.ts │ │ ├── stdio.ts │ │ ├── dxt.ts │ │ ├── locale.ts │ │ ├── snackbar.ts │ │ ├── history.ts │ │ ├── prompt.ts │ │ ├── chatbot.ts │ │ └── resource.ts │ ├── index.html │ ├── plugins │ │ ├── i18n.ts │ │ └── vuetify.ts │ ├── main.ts │ ├── App.vue │ ├── router │ │ └── index.ts │ └── locales │ │ └── zh.json ├── main │ ├── aid │ │ ├── index.d.ts │ │ ├── config.ts │ │ ├── utils.ts │ │ ├── types.ts │ │ ├── window.ts │ │ ├── nut.ts │ │ ├── shortcuts.ts │ │ ├── automator.ts │ │ ├── windows.ts │ │ ├── automation.ts │ │ └── commander.ts │ ├── assets │ │ ├── icon │ │ │ ├── icon.png │ │ │ ├── icon@2x.png │ │ │ ├── icon@4x.png │ │ │ ├── icon_raw.png │ │ │ ├── icon@1.25x.png │ │ │ └── icon@1.5x.png │ │ └── config │ │ │ ├── mcp.json │ │ │ ├── popup.json │ │ │ └── llm.json │ ├── utils │ │ ├── Util.ts │ │ ├── notification.ts │ │ └── Constants.ts │ ├── types.ts │ ├── mcp │ │ ├── connection.ts │ │ ├── types.ts │ │ ├── init.ts │ │ ├── dxt.ts │ │ ├── config.ts │ │ └── client.ts │ └── tray.ts ├── types │ ├── popup.d.ts │ ├── startup.d.ts │ ├── llm.d.ts │ ├── ipc.d.ts │ └── mcp.d.ts ├── vue-shim.d.ts └── types.d.ts ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── fix_typo.yml │ ├── feature_request.yml │ └── bug_report.yml ├── FUNDING.yml ├── pull_request_template.md └── workflows │ ├── build-artifacts.yml │ └── playwright-test.yml ├── .vscode ├── extensions.json ├── launch.json ├── tasks.json └── settings.json ├── .prettierignore ├── .editorconfig ├── playwright.config.ts ├── npmrc.template ├── .prettierrc ├── tsconfig.node.json ├── tsconfig.json ├── SECURITY.md ├── tests ├── testUtil.mts ├── specs │ └── app.spec.ts └── fixtures.mts ├── .gitignore ├── eslint.config.mjs ├── AGENTS.md └── vite.config.mts /docs/.nvmrc: -------------------------------------------------------------------------------- 1 | 22.17.0 -------------------------------------------------------------------------------- /src/renderer/public/images/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/src/en/electron-how-to/index.md: -------------------------------------------------------------------------------- 1 | # Electron How-to 2 | -------------------------------------------------------------------------------- /docs/src/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /docs/src/zhHans/project-structures/index.md: -------------------------------------------------------------------------------- 1 | # 项目结构 2 | -------------------------------------------------------------------------------- /src/main/aid/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'applescript' 2 | -------------------------------------------------------------------------------- /docs/src/zhHans/electron-how-to/index.md: -------------------------------------------------------------------------------- 1 | # Electron 操作方法 2 | -------------------------------------------------------------------------------- /docs/src/zhHans/installation-and-build/index.md: -------------------------------------------------------------------------------- 1 | # 安装和构建 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /docs/src/en/project-structures/index.md: -------------------------------------------------------------------------------- 1 | # Project Structures 2 | -------------------------------------------------------------------------------- /docs/src/en/installation-and-build/index.md: -------------------------------------------------------------------------------- 1 | # Installation and Build 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/src/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI-QL/tuui/HEAD/docs/src/public/icon.png -------------------------------------------------------------------------------- /docs/src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI-QL/tuui/HEAD/docs/src/public/favicon.ico -------------------------------------------------------------------------------- /src/main/assets/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI-QL/tuui/HEAD/src/main/assets/icon/icon.png -------------------------------------------------------------------------------- /src/renderer/screens/ErrorScreen.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/main/assets/icon/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI-QL/tuui/HEAD/src/main/assets/icon/icon@2x.png -------------------------------------------------------------------------------- /src/main/assets/icon/icon@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI-QL/tuui/HEAD/src/main/assets/icon/icon@4x.png -------------------------------------------------------------------------------- /src/main/assets/icon/icon_raw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI-QL/tuui/HEAD/src/main/assets/icon/icon_raw.png -------------------------------------------------------------------------------- /src/main/assets/icon/icon@1.25x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI-QL/tuui/HEAD/src/main/assets/icon/icon@1.25x.png -------------------------------------------------------------------------------- /src/main/assets/icon/icon@1.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AI-QL/tuui/HEAD/src/main/assets/icon/icon@1.5x.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: AI-QL 4 | custom: ["https://www.aiql.com"] 5 | -------------------------------------------------------------------------------- /docs/src/zhHans/other-projects.md: -------------------------------------------------------------------------------- 1 | # 其他项目 2 | 3 | ## 在寻找简化的聊天机器人用户界面? 4 | 5 | 也可以查看 `Chat-UI` 和 `Chat-MCP` 项目。 6 | 7 | https://github.com/AI-QL 8 | -------------------------------------------------------------------------------- /src/renderer/components/layouts/FooterLayout.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Lock files 2 | *-lock.json 3 | 4 | # IDEs 5 | .idea/ 6 | .vscode/ 7 | 8 | # Project files 9 | .github/ 10 | buildAssets/icons/ 11 | dist/ 12 | release/ 13 | -------------------------------------------------------------------------------- /docs/src/en/other-projects.md: -------------------------------------------------------------------------------- 1 | # Other Projects 2 | 3 | ## Looking for simplified UI for Chatbot? 4 | 5 | Also check out the `Chat-UI` and `Chat-MCP` project. 6 | 7 | https://github.com/AI-QL 8 | -------------------------------------------------------------------------------- /src/types/popup.d.ts: -------------------------------------------------------------------------------- 1 | export interface PopupConfig { 2 | prompts: PopupPromptsType[] 3 | } 4 | 5 | export interface PopupPromptsType { 6 | icon: string 7 | title: string 8 | prompt: string 9 | } 10 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Node artifact files 2 | node_modules/ 3 | 4 | # VitePress files 5 | dist 6 | dist/* 7 | src/.vitepress/.temp 8 | src/.vitepress/.temp/* 9 | src/.vitepress/cache 10 | src/.vitepress/cache/* 11 | -------------------------------------------------------------------------------- /src/vue-shim.d.ts: -------------------------------------------------------------------------------- 1 | // For fix typescript import errors 2 | declare module '*.vue' { 3 | import { defineComponent } from 'vue' 4 | const component: ReturnType 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/composables/useRules.ts: -------------------------------------------------------------------------------- 1 | import { useI18n } from 'vue-i18n' 2 | 3 | export function useRules() { 4 | const { t } = useI18n() 5 | return { 6 | required: (v: string | undefined) => !!v || t('dxt.required') 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@modelcontextprotocol/sdk/types' { 2 | export * from '@modelcontextprotocol/sdk/types.d.ts' 3 | } 4 | 5 | declare module 'vuetify/lib/util/colors' { 6 | export * from 'vuetify/lib/util/colors.js' 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/utils/color.ts: -------------------------------------------------------------------------------- 1 | export const secondaryColor = 2 | "data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Crect width='10' height='6' fill='rgb(1,87,155)'/%3E%3C/svg%3E" 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | max_line_length = 100 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /src/renderer/screens/chat/ChatInputScreen.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | -------------------------------------------------------------------------------- /src/types/startup.d.ts: -------------------------------------------------------------------------------- 1 | export interface StartupConfig { 2 | news: StartupNewsType[] 3 | } 4 | 5 | export interface StartupNewsType { 6 | img: string 7 | title: string 8 | subtitle: string 9 | duration: string 10 | link: string 11 | cover?: boolean 12 | } 13 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "resolveJsonModule": true, 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["src/.vitepress/config.mts", "package.json"] 10 | } 11 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@playwright/test' 2 | 3 | export default defineConfig({ 4 | outputDir: 'tests/results', 5 | retries: process.env.CI ? 2 : 0, 6 | workers: process.env.CI ? 1 : undefined, 7 | timeout: 60000, 8 | expect: { 9 | timeout: 10000 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /src/main/utils/Util.ts: -------------------------------------------------------------------------------- 1 | export function debounce(func, timeout = 200) { 2 | let timer 3 | return (function (...args) { 4 | if (!timer) { 5 | func.apply(this, args) 6 | } 7 | clearTimeout(timer) 8 | timer = setTimeout(() => { 9 | timer = undefined 10 | }, timeout) 11 | })() 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/components/layouts/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultLayout from '@/renderer/components/layouts/DefaultLayout.vue' 2 | import HeaderLayout from '@/renderer/components/layouts/HeaderLayout.vue' 3 | import SidebarLayout from '@/renderer/components/layouts/SidebarLayout.vue' 4 | 5 | export { DefaultLayout, HeaderLayout, SidebarLayout } 6 | -------------------------------------------------------------------------------- /src/renderer/types/session.ts: -------------------------------------------------------------------------------- 1 | import type { ChatConversationMessage } from '@/renderer/types/message' 2 | 3 | import { Tool as McpToolMessage } from '@modelcontextprotocol/sdk/types' 4 | 5 | export type SessionId = string 6 | 7 | export type { McpToolMessage } 8 | 9 | export type SessionEntry = { 10 | id: SessionId 11 | messages: ChatConversationMessage[] 12 | tools?: McpToolMessage[] 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/store/counter.ts: -------------------------------------------------------------------------------- 1 | // This is a template store 2 | import { defineStore } from 'pinia' 3 | 4 | export const useCounterStore = defineStore('counterStore', { 5 | state: () => ({ 6 | counter: 0 7 | }), 8 | getters: { 9 | getCounter: (state): number => state.counter 10 | }, 11 | actions: { 12 | counterIncrease(amount: number) { 13 | this.counter += amount 14 | } 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /npmrc.template: -------------------------------------------------------------------------------- 1 | # Copy or rename this file to .npmrc to enable the mirror registry 2 | # Example: 3 | # cp npmrc.template .npmrc (Linux / macOS) 4 | # copy npmrc.template .npmrc (Windows cmd) 5 | # WARNING: Do NOT commit authentication tokens or private credentials to the repo. 6 | ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ 7 | ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ 8 | -------------------------------------------------------------------------------- /src/main/assets/config/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "everything": { 4 | "command": "npx", 5 | "args": ["-y", "@modelcontextprotocol/server-everything"] 6 | }, 7 | "tuui Docs": { 8 | "command": "npx", 9 | "args": ["mcp-remote", "https://gitmcp.io/AI-QL/tuui"] 10 | }, 11 | "antvis-chart": { 12 | "command": "npx", 13 | "args": ["-y", "@antv/mcp-server-chart"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "semi": false, 4 | "vueIndentScriptAndStyle": false, 5 | "singleQuote": true, 6 | "quoteProps": "as-needed", 7 | "trailingComma": "none", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "jsxSingleQuote": false, 11 | "arrowParens": "always", 12 | "insertPragma": false, 13 | "requirePragma": false, 14 | "proseWrap": "never", 15 | "htmlWhitespaceSensitivity": "strict", 16 | "endOfLine": "lf" 17 | } 18 | -------------------------------------------------------------------------------- /docs/src/zhHans/introduction.md: -------------------------------------------------------------------------------- 1 | # 导言 2 | 3 | **TUUI** 是基于 MCP(模型上下文协议)的 AI 工具整合应用,用于开发智能跨平台桌面应用。它使用 `Vue 3`,使您能够轻松构建快速的开发环境。 4 | 5 | ## 使用优势 6 | 7 | - ✅ 无需任何预设,即可立即构建,快速开发。 8 | - ✅ 快速维护,与最新的 `Vue` 和 `Electron` 以及许多模块兼容。 9 | - ✅ 通过使用各种附加模板,无需担心布局和数据管理。 10 | 11 | ## 特点 12 | 13 | - ✨ 通过 MCP 协议快速对接 AI 工具 14 | - ✨ 动态管理不同厂商的语言模型后端 API 15 | - ✨ 跨平台开发和构建支持 16 | - ✨ 支持自动化应用程序测试 17 | - ✨ 支持 TypeScript 18 | - ✨ 多语言支持 19 | - ✨ 基本布局管理器 20 | - ✨ 通过 Pinia 存储进行全局状态管理 21 | - ✨ 通过 GitHub 社区和官方文档提供快速支持 22 | -------------------------------------------------------------------------------- /docs/src/zhHans/installation-and-build/install-local-documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 5 3 | --- 4 | 5 | # 管理本地文档 6 | 7 | TUUI 中的文档可以通过 VitePress 查看器在本地环境中查看。 8 | 9 | ## 安装 10 | 11 | 以下说明中的所有操作均应在"文档"文件夹中完成。 12 | 13 | ```shell 14 | $ cd docs 15 | ``` 16 | 17 | 使用以下命令安装相关软件包: 18 | 19 | ```shell 20 | # via npm 21 | $ npm i 22 | 23 | # via yarn (https://yarnpkg.com) 24 | $ yarn install 25 | 26 | # via pnpm (https://pnpm.io) 27 | $ pnpm i 28 | ``` 29 | 30 | 您可以通过以下命令运行托管文档的本地服务器。 31 | 32 | ```shell 33 | $ npm run dev 34 | ``` 35 | -------------------------------------------------------------------------------- /src/renderer/composables/useRouteFeatures.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useRoute } from 'vue-router' 3 | import { ComponentName } from '@/renderer/screens' 4 | 5 | export function useRouteFeatures() { 6 | const route = useRoute() 7 | 8 | const hasComponent = (componentName: ComponentName) => { 9 | return computed(() => 10 | route.matched.some((record) => Boolean(record.components?.[componentName])) 11 | ) 12 | } 13 | 14 | const titleKey = computed(() => { 15 | return route.meta?.titleKey || 'title.main' 16 | }) 17 | 18 | return { hasComponent, titleKey } 19 | } 20 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tuui-docs", 3 | "private": true, 4 | "version": "1.3.6", 5 | "scripts": { 6 | "dev": "vitepress dev src", 7 | "build": "vitepress build src", 8 | "serve": "vitepress serve src" 9 | }, 10 | "author": "AIQL ", 11 | "license": "Apache-2.0", 12 | "engines": { 13 | "node": ">=22.12.0" 14 | }, 15 | "dependencies": { 16 | "vitepress": "^2.0.0-alpha.15", 17 | "vitepress-i18n": "^1.3.4", 18 | "vitepress-sidebar": "^1.33.0", 19 | "vue": "^3.5.25" 20 | }, 21 | "optionalDependencies": { 22 | "@rollup/rollup-linux-x64-gnu": "4.53.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/aid/config.ts: -------------------------------------------------------------------------------- 1 | export type Shortcut = { 2 | alt?: boolean 3 | ctrl?: boolean 4 | shift?: boolean 5 | meta?: boolean 6 | key: string 7 | [key: string]: boolean | string 8 | } 9 | 10 | export const disabledShortcutKey: string = 'none' 11 | 12 | export type ShortcutsConfig = { 13 | command: Shortcut 14 | } 15 | 16 | export type Configuration = { 17 | shortcuts: ShortcutsConfig 18 | } 19 | 20 | export const loadSettings = (): Configuration => { 21 | return { 22 | shortcuts: { 23 | command: { 24 | key: 'T', // Ctrl-Alt-T 25 | alt: true, 26 | ctrl: true 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/screens/agent/AgentSideDock.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/src/zhHans/installation-and-build/automated-testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 4 3 | --- 4 | 5 | # 自动测试 6 | 7 | **TUUI**包括自动测试。测试框架使用微软的 [Playwright](https://playwright.dev)。 8 | 9 | **Playwright**针对网络应用测试进行了优化,并完全支持**Electron**框架。它易于安装,无需配置即可立即开始测试,并且是跨平台的。您可以在此处了解有关**Playwright**的更多信息:https://github.com/microsoft/playwright 10 | 11 | 此模板仅对模板主屏幕进行了非常简单的启动和行为测试。高级测试取决于您的应用程序范围。 12 | 13 | 目前,测试规范文件位于`tests`目录中,测试结果文件位于`tests/results`中。(内置测试规范文件不会生成单独的结果文件。) 14 | 15 | Playwright配置文件位于项目根目录下的playwright.config.ts,更多信息请参阅以下文档:https://playwright.dev/docs/test-configuration 16 | 17 | 完成所有配置后,您可以使用以下命令进行测试。 18 | 19 | ```shell 20 | $ npm run test 21 | ``` 22 | 23 | 在运行测试之前,请清空构建目录(`dist`)并编译测试包。 24 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/renderer/screens/chat/ChatEndScreen.vue: -------------------------------------------------------------------------------- 1 | 5 | 22 | -------------------------------------------------------------------------------- /src/renderer/public/flags/us.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClientResult as McpClientResponse, 3 | CreateMessageRequest as SamplingRequest, 4 | CreateMessageResult as SamplingResponse, 5 | ElicitRequest, 6 | ElicitResult as ElicitResponse 7 | } from '@modelcontextprotocol/sdk/types.js' 8 | 9 | export type CommandResponse = { 10 | prompt: string 11 | id: string 12 | } 13 | 14 | export type CommandRequest = { 15 | prompt: string 16 | input: string 17 | } 18 | 19 | export type McpInitResponse = 20 | | { 21 | status: 'success' 22 | } 23 | | { 24 | status: 'error' 25 | error: string 26 | } 27 | 28 | export { McpClientResponse, SamplingRequest, SamplingResponse, ElicitRequest, ElicitResponse } 29 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "compounds": [ 4 | { 5 | "name": "Debug Run", 6 | "configurations": [ 7 | "Debug App" 8 | ], 9 | "presentation": { 10 | "hidden": false, 11 | "group": "", 12 | "order": 1 13 | }, 14 | "stopAll": true 15 | } 16 | ], 17 | "configurations": [ 18 | { 19 | "name": "Debug App", 20 | "request": "launch", 21 | "type": "node", 22 | "timeout": 60000, 23 | "runtimeArgs": [ 24 | "run-script", 25 | "dev" 26 | ], 27 | "cwd": "${workspaceRoot}", 28 | "runtimeExecutable": "npm", 29 | "console": "integratedTerminal" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "jsx": "preserve", 6 | "moduleResolution": "bundler", 7 | "composite": true, 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "emitDeclarationOnly": true, 12 | "allowImportingTsExtensions": true, 13 | "paths": { 14 | "@/*": ["./src/*"] 15 | } 16 | }, 17 | "include": [ 18 | "src/main", 19 | "src/types", 20 | "src/preload", 21 | "package.json", 22 | "vite.config.mts", 23 | "buildAssets/builder", 24 | "tests/**/*.ts", 25 | "tests/**/*.mts", 26 | "tests/**/*.spec.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /docs/src/en/installation-and-build/install-local-documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 5 3 | --- 4 | 5 | # Manage Local Documentation 6 | 7 | Documents from `TUUI` can be viewed in the local environment through the `VitePress` viewer. 8 | 9 | ## Installation 10 | 11 | Everything in the instructions below should be done in the `docs` folder. 12 | 13 | ```shell 14 | $ cd docs 15 | ``` 16 | 17 | Install the relevant packages using the following commands: 18 | 19 | ```shell 20 | # via npm 21 | $ npm i 22 | 23 | # via yarn (https://yarnpkg.com) 24 | $ yarn install 25 | 26 | # via pnpm (https://pnpm.io) 27 | $ pnpm i 28 | ``` 29 | 30 | You can run the local server where the documents are hosted via the command below. 31 | 32 | ```shell 33 | $ npm run dev 34 | ``` 35 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Use 'Ctrl-Shift-P', select 'Tasks: Run Task' to show all diff and use AI tool to generate commit message. 2 | { 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "label": "Run Git Diff on Staged Files", 7 | "type": "shell", 8 | "command": "git", 9 | "args": ["--no-pager", "diff", "--staged"], 10 | "group": "build", 11 | "presentation": { 12 | "echo": true, 13 | "reveal": "always", 14 | "focus": false, 15 | "panel": "dedicated", 16 | "showReuseMessage": false, 17 | "clear": false 18 | }, 19 | "problemMatcher": [] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.settings.useSplitJSON": true, 3 | "css.lint.validProperties": [ 4 | "app-region" 5 | ], 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": "explicit" 8 | }, 9 | "emmet.syntaxProfiles": {}, 10 | "files.autoSave": "afterDelay", 11 | "editor.wordWrap": "on", 12 | "editor.formatOnSave": false, 13 | "editor.tabSize": 2, 14 | "[jsonc]": { 15 | "editor.defaultFormatter": "vscode.json-language-features" 16 | }, 17 | "[typescript]": { 18 | "editor.defaultFormatter": "vscode.typescript-language-features" 19 | }, 20 | "[json]": { 21 | "editor.defaultFormatter": "vscode.json-language-features" 22 | }, 23 | "[javascript]": { 24 | "editor.defaultFormatter": "vscode.typescript-language-features" 25 | } 26 | } -------------------------------------------------------------------------------- /src/types/llm.d.ts: -------------------------------------------------------------------------------- 1 | export interface LlmConfig { 2 | default: ChatbotConfig 3 | custom: ChatbotConfig[] 4 | } 5 | 6 | export interface ChatbotConfig { 7 | name: string 8 | 9 | apiKey: string 10 | apiCli: string 11 | 12 | icon: string 13 | 14 | url: string 15 | urlList: string[] 16 | 17 | path: string 18 | pathList: string[] 19 | 20 | model: string 21 | modelList: string[] 22 | 23 | authPrefix: string 24 | authPrefixList: string[] 25 | 26 | maxTokensValue?: string 27 | maxTokensPrefix: string 28 | maxTokensPrefixList: string[] 29 | 30 | temperature?: string 31 | topP?: string 32 | method: string 33 | contentType: string 34 | stream: boolean 35 | reasoningEffort?: number 36 | enableThinking?: number 37 | authorization: boolean 38 | mcp: boolean 39 | } 40 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "outDir": "./dist", 5 | "target": "esnext", 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "jsx": "preserve", 9 | "noImplicitAny": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "allowSyntheticDefaultImports": true, 13 | "declaration": true, 14 | "resolveJsonModule": true, 15 | "esModuleInterop": true, 16 | "sourceMap": true, 17 | "strict": true, 18 | "skipLibCheck": true, 19 | "paths": { 20 | "@/*": ["./src/*"] 21 | }, 22 | "lib": ["esnext", "dom"] 23 | }, 24 | "include": ["src/*.ts", "src/*.d.ts", "src/renderer"], 25 | "references": [ 26 | { 27 | "path": "./tsconfig.node.json" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/components/common/LogoAvatar.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/renderer/components/layouts/SidebarLayout.vue: -------------------------------------------------------------------------------- 1 | 17 | 30 | -------------------------------------------------------------------------------- /src/renderer/store/layout.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export type RoutePath = '/' | '/chat' | '/agent' | '/setting' 4 | 5 | const PATH_TO_SCREEN = { 6 | '/': 0, 7 | '/chat': 1, 8 | '/agent': 2, 9 | '/setting': 3 10 | } as Record 11 | 12 | type ScreenKey = keyof typeof PATH_TO_SCREEN 13 | type ScreenValue = (typeof PATH_TO_SCREEN)[ScreenKey] 14 | 15 | type LoadingStatus = 'start' | 'stop' | false 16 | 17 | export const getScreenFromPath = (path: string): ScreenValue => { 18 | return PATH_TO_SCREEN[path as ScreenKey] ?? 0 19 | } 20 | 21 | export const useLayoutStore = defineStore('layoutStore', { 22 | state: () => ({ 23 | sidebar: true, 24 | apiKeyShow: false, 25 | mcpLoading: false as LoadingStatus, 26 | screen: 0 // The selected screen is a list: 0,1,2,... 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/fix_typo.yml: -------------------------------------------------------------------------------- 1 | name: 'Fix typo request' 2 | description: Request to fix a typo or bad translation in this project 3 | labels: ['typo'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Before creating an issue, please read the following: 9 | 10 | - Search to see if the same issue already exists, and keep the title concise and accurate so that it's easy for others to understand and search for. 11 | - Please create a separate issue for each type of issue. 12 | - Please be as detailed as possible and write in English so that we can handle your issue quickly. 13 | - type: textarea 14 | attributes: 15 | label: Describe the issue 16 | description: Please describe where the typo occurs and a list of text that needs to be corrected. 17 | validations: 18 | required: true 19 | -------------------------------------------------------------------------------- /src/renderer/public/flags/cn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/renderer/components/common/CommandCard.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 27 | -------------------------------------------------------------------------------- /src/renderer/types/registry.ts: -------------------------------------------------------------------------------- 1 | // Define the structure of a registry package 2 | export type McpRegistryPackage = { 3 | identifier: string 4 | registryType: 'mcpb' | 'npm' | 'oci' | 'pypi' | string 5 | registryBaseUrl?: string 6 | transport?: { 7 | type: string 8 | } 9 | } 10 | 11 | // Define the structure of a registry server 12 | export type McpRegistryServer = { 13 | server: { 14 | name: string 15 | description: string 16 | version: string 17 | repository: { 18 | source: string 19 | url: string 20 | } 21 | packages: McpRegistryPackage[] 22 | remotes: { 23 | url: string 24 | type: string 25 | }[] 26 | } 27 | } 28 | 29 | // Define the structure of a registry type 30 | export type McpRegistryType = { 31 | servers: McpRegistryServer[] 32 | metadata: { 33 | nextCursor: string 34 | count: number 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/src/zhHans/project-structures/specification-references.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 2 3 | --- 4 | 5 | # 参考规范文档 6 | 7 | ## 网络应用框架 8 | 9 | - [Vite](https://vitejs.dev) 10 | - [Electron](https://www.electronjs.org) 11 | - [Electron Builder](https://www.electron.build) 12 | - [Vutron](https://vutron.cdget.com) 13 | 14 | ## 开发帮助工具 15 | 16 | - [TypeScript](https://www.typescriptlang.org) 17 | - [ESLint](https://eslint.org) 18 | - [Prettier](https://prettier.io) 19 | 20 | ## 前端框架(Vue) 21 | 22 | - [Vue](https://vuejs.org) 23 | - [Vue-i18n](https://kazupon.github.io/vue-i18n) 24 | - [Vue-router](https://router.vuejs.org) 25 | - [Pinia](https://pinia.vuejs.org) 26 | - [Pinia Persistedstate](https://prazdevs.github.io/pinia-plugin-persistedstate) 27 | 28 | ## 设计框架 29 | 30 | - [Vuetify](https://vuetifyjs.com) 31 | - [MD Editor V3](https://imzbf.github.io/md-editor-v3) 32 | 33 | ## 测试 34 | 35 | - [Playwright](https://playwright.dev) 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 'Feature request' 2 | description: Report a feature request in this project. 3 | labels: ['enhancement'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Before creating an issue, please read the following: 9 | 10 | - Search to see if the same issue already exists, and keep the title concise and accurate so that it's easy for others to understand and search for. 11 | - Please create a separate issue for each type of issue. 12 | - Please be as detailed as possible and write in English so that we can handle your issue quickly. 13 | - type: textarea 14 | attributes: 15 | label: Describe the feature 16 | description: Feel free to describe any features or improvements you would like to see. You can attach text or images of examples, behavior, etc. from other projects to elaborate. 17 | validations: 18 | required: true 19 | -------------------------------------------------------------------------------- /docs/src/en/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | **TUUI** is desktop application based on MCP(Model Context Protocol). It uses `Vue 3` and allows you to build a fast development environment with little effort. 4 | 5 | ## Advantages of use 6 | 7 | - ✅ You can build immediately without any presets, so you can develop quickly. 8 | - ✅ It is being maintained quickly to be compatible with the latest `Vue` and `Electron`, as well as many modules. 9 | - ✅ There is no need to worry about layout and data management by using various additional templates. 10 | 11 | ## Features 12 | 13 | - ✨ Accelerate AI tool integration via MCP 14 | - ✨ Orchestrate cross-vendor LLM APIs through dynamic configuring 15 | - ✨ Automated application testing Support 16 | - ✨ TypeScript support 17 | - ✨ Multilingual support 18 | - ✨ Basic layout manager 19 | - ✨ Global state management through the Pinia store 20 | - ✨ Quick support through the GitHub community and official documentation 21 | -------------------------------------------------------------------------------- /src/renderer/screens/chat/ChatScreen.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /src/renderer/screens/setting/SettingScreen.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 31 | -------------------------------------------------------------------------------- /docs/src/en/project-structures/specification-references.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 2 3 | --- 4 | 5 | # Specification References 6 | 7 | ## Web app frameworks 8 | 9 | - [Vite](https://vitejs.dev) 10 | - [Electron](https://www.electronjs.org) 11 | - [Electron Builder](https://www.electron.build) 12 | - [Vutron](https://vutron.cdget.com) 13 | 14 | ## Development help tools 15 | 16 | - [TypeScript](https://www.typescriptlang.org) 17 | - [ESLint](https://eslint.org) 18 | - [Prettier](https://prettier.io) 19 | 20 | ## Front-end frameworks (Vue) 21 | 22 | - [Vue](https://vuejs.org) 23 | - [Vue-i18n](https://kazupon.github.io/vue-i18n) 24 | - [Vue-router](https://router.vuejs.org) 25 | - [Pinia](https://pinia.vuejs.org) 26 | - [Pinia Persistedstate](https://prazdevs.github.io/pinia-plugin-persistedstate) 27 | 28 | ## Design frameworks 29 | 30 | - [Vuetify](https://vuetifyjs.com) 31 | - [MD Editor V3](https://imzbf.github.io/md-editor-v3) 32 | 33 | ## Testing 34 | 35 | - [Playwright](https://playwright.dev) 36 | -------------------------------------------------------------------------------- /src/renderer/components/pages/McpConfigPage.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /docs/src/zhHans/electron-how-to/preload-script.md: -------------------------------------------------------------------------------- 1 | # 预加载脚本 2 | 3 | Electron.js中的预加载脚本是一个安全区域,用于主进程和渲染器进程之间的通信。它通常用于 **[IPC通信](https://www.electronjs.org/docs/latest/tutorial/ipc)**。 4 | 5 | 更多信息,请参阅以下文章: https://www.electronjs.org/docs/latest/tutorial/tutorial-preload 6 | 7 | 为了与最新版本的Electron兼容并确保安全,我们不建议使用旧的`electron/remote`模块。如果您想使用系统事件或Node脚本,建议在主进程中使用,而不是在渲染器中。 8 | 9 | TUUI 的预加载脚本位于`src/preload`文件夹中。要创建新的IPC通信通道,请将通道名称添加到以下变量中,将其列入通信白名单。 10 | 11 | - `mainAvailChannels`: 从主程序发送事件到渲染程序。 (`window.mainApi.send('channelName')`) 12 | - `rendererAvailChannels`: 将事件从渲染器发送到主程序。 (`mainWindow.webContents.send('channelName')`) 13 | 14 | 当从渲染器向主程序发送事件时,应访问`window.mainApi`对象,而不是`ipcRenderer.send`。`mainApi`是模板中仅用于演示的占位名称,请勿在生产环境相关核心功能中使用该接口。对于MCP服务器等实际集成场景,请改用专用的 `mcpServers` API。 15 | 16 | 以下是mainApi支持的功能: 17 | 18 | - `send`: 将活动发送至主页面。 19 | - `on`: 一个接收主发送事件的听众。 20 | - `once`: 接听主叫方发送的事件。(仅处理一个呼叫) 21 | - `off`: 移除事件监听器 22 | - `invoke`: 可异步发送事件和接收数据的功能。 23 | 24 | 要更改和修改此设置,您需要修改 `src/preload/index.ts` 中的 `exposeInMainWorld`。 25 | -------------------------------------------------------------------------------- /src/main/aid/utils.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | 3 | export type anyDict = Record 4 | export type strDict = Record 5 | const textCache: strDict = {} 6 | 7 | export const getCachedText = (id: string): string => { 8 | const prompt = textCache[id] 9 | delete textCache[id] 10 | return prompt 11 | } 12 | 13 | export const putCachedText = (text: string): string => { 14 | Object.keys(textCache).forEach((key) => { 15 | delete textCache[key] 16 | }) 17 | 18 | const id = uuidv4() 19 | textCache[id] = text 20 | return id 21 | } 22 | 23 | export const wait = async (millis = 200) => { 24 | if (process.env.DEBUG && process.platform !== 'darwin') { 25 | // for an unknown reason, the promise code started to fail when debugging on Windows/Linux 26 | const waitTill = new Date().getTime() + millis 27 | while (waitTill > new Date().getTime()) { 28 | // do nothing 29 | } 30 | } else { 31 | await new Promise((resolve) => setTimeout(resolve, millis)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting Security Issues 4 | 5 | For general security concerns without critical risks, feel free to submit a regular GitHub issue through: https://github.com/AI-QL/tuui/issues/new/choose 6 | 7 | For potential security vulnerabilities requiring confidential handling (especially those with sensitive exploit details), please prioritize private disclosure by contacting maintainers directly via email at contact@aiql.com. Avoid public issue discussions for these cases. 8 | 9 | ## Security compliance 10 | 11 | Project maintainers are quickly addressing reported security vulnerabilities in the project and providing relevant patches. 12 | 13 | We report these to the relevant users and handle the correspondence to prevent the issue from recurring. 14 | 15 | ## Security recommendations 16 | 17 | We recommend that users of project sources use the latest version, which addresses possible security vulnerabilities. 18 | 19 | ## Contact 20 | 21 | - Administrator: contact@aiql.com 22 | -------------------------------------------------------------------------------- /src/renderer/plugins/i18n.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import en from '@/renderer/locales/en.json' 3 | import zh from '@/renderer/locales/zh.json' 4 | // import ko from '@/renderer/locales/ko.json' 5 | // import zhHant from '@/renderer/locales/zh-hant.json' 6 | // import de from '@/renderer/locales/de.json' 7 | // import es from '@/renderer/locales/es.json' 8 | // import ja from '@/renderer/locales/ja.json' 9 | // import fr from '@/renderer/locales/fr.json' 10 | // import ru from '@/renderer/locales/ru.json' 11 | // import pt from '@/renderer/locales/pt.json' 12 | // import nl from '@/renderer/locales/nl.json' 13 | import { getCurrentLocale } from '@/renderer/utils' 14 | 15 | export default createI18n({ 16 | legacy: false, 17 | locale: getCurrentLocale(), 18 | fallbackLocale: 'en', 19 | globalInjection: true, 20 | messages: { 21 | en, 22 | zh 23 | 24 | // ko, 25 | // zhHant, 26 | // de, 27 | // es, 28 | // ja, 29 | // fr, 30 | // ru, 31 | // pt, 32 | // nl, 33 | // zhHans, 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /src/renderer/components/common/ImgDialog.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | 40 | 47 | -------------------------------------------------------------------------------- /src/renderer/components/pages/McpRegistryPage.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 46 | 47 | -------------------------------------------------------------------------------- /tests/testUtil.mts: -------------------------------------------------------------------------------- 1 | import { Page } from 'playwright' 2 | import { TestInfo } from 'playwright/test' 3 | 4 | export default class TestUtil { 5 | _page: Page 6 | 7 | _testInfo: TestInfo 8 | 9 | _testScreenshotPath: string 10 | 11 | constructor(page: Page, testInfo: TestInfo, testScreenshotPath: string) { 12 | this._page = page 13 | this._testInfo = testInfo 14 | this._testScreenshotPath = testScreenshotPath 15 | } 16 | 17 | async captureScreenshot(pageInstance: Page, screenshotName: string) { 18 | if (!pageInstance) { 19 | return 20 | } 21 | 22 | try { 23 | const screenshotPath = `${this._testScreenshotPath}/${screenshotName || `unknown_${Date.now()}`}.png` 24 | 25 | await pageInstance.screenshot({ path: screenshotPath }) 26 | } catch (error) { 27 | // Do nothing 28 | } 29 | } 30 | 31 | async onTestError(error: Error) { 32 | const titleLists = [...this._testInfo.titlePath] 33 | titleLists.shift() 34 | const title = titleLists.join('-') 35 | 36 | await this.captureScreenshot(this._page, `${title}_${Date.now()}`) 37 | 38 | return new Error(error.message) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/screens/setting/SettingMenuScreen.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # .gitignore for Node.js Projects 2 | # ---------- Start of common ignore files 3 | 4 | # Node artifact files 5 | node_modules/ 6 | 7 | # Desktop extensions 8 | src/main/assets/mcpb 9 | 10 | # Log files 11 | *.log 12 | 13 | # dotenv environment variables file 14 | .env 15 | .npmrc 16 | 17 | # JetBrains IDEs 18 | .idea/ 19 | *.iml 20 | 21 | # Visual Studio Code IDE 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | !.vscode/*.code-snippets 28 | 29 | # Local History for Visual Studio Code 30 | .history/ 31 | 32 | # Built Visual Studio Code Extensions 33 | *.vsix 34 | 35 | # Generated by MacOS 36 | .DS_Store 37 | .AppleDouble 38 | .LSOverride 39 | 40 | # Generated by Windows 41 | Thumbs.db 42 | [Dd]esktop.ini 43 | $RECYCLE.BIN/ 44 | 45 | # Applications 46 | *.app 47 | *.pkg 48 | *.dmg 49 | *.exe 50 | *.war 51 | *.deb 52 | 53 | # Large media files 54 | *.mp4 55 | *.tiff 56 | *.avi 57 | *.flv 58 | *.mov 59 | *.wmv 60 | 61 | # ---------- End of common ignore files 62 | 63 | # Project Files 64 | dist/ 65 | release/ 66 | tests/results/ 67 | npm-debug.log 68 | npm-debug.log.* 69 | vite-plugin-electron.log 70 | -------------------------------------------------------------------------------- /src/renderer/store/stdio.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref, toRaw } from 'vue' 3 | 4 | import type { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js' 5 | 6 | // Other keyof StdioServerParameters are not in use 7 | export type StdioServerKey = 'command' | 'args' | 'env' 8 | 9 | export type CustomStdioServerParameters = Partial 10 | 11 | export const useStdioStore = defineStore( 12 | 'stdioStore', 13 | () => { 14 | const configValues = ref>({}) 15 | 16 | function updateConfigAttribute( 17 | name: string, 18 | key: K, 19 | value: StdioServerParameters[K] 20 | ) { 21 | if (!configValues.value[name]) { 22 | configValues.value[name] = {} 23 | } 24 | configValues.value[name][key] = value 25 | } 26 | 27 | const getConfig = (name: string) => { 28 | return toRaw(configValues.value[name]) ?? null 29 | } 30 | 31 | function deleteConfig(name: string) { 32 | delete configValues.value[name] 33 | } 34 | 35 | return { configValues, getConfig, deleteConfig, updateConfigAttribute } 36 | }, 37 | { 38 | persist: true 39 | } 40 | ) 41 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Pull request checklist 8 | 9 | You should familiarize yourself with the files `README.md`, `CONTRIBUTING.md`, and `CODE_OF_CONDUCT.md` in the root of your project. 10 | 11 | - If an issue has been created for this, add `(fixes #{ISSUE_NUMBER})` to the end of the commit description. In `{ISSUE_NUMBER}`, please include the relevant issue number. 12 | - If you need to update or add to the article, please update the relevant content. If a multilingual article exists, you should update all relevant content in your own language, except for translations. 13 | - Add or update test code if it exists and is needed. Also, verify that the tests pass. 14 | - If this PR is not yet complete, keep the PR in draft status. If it's no longer valid, close the PR with an explanation. 15 | 16 | 19 | 20 | ### What did you change? 21 | 22 | ### Why did you make the change? 23 | 24 | ### How does this work? 25 | -------------------------------------------------------------------------------- /src/main/aid/types.ts: -------------------------------------------------------------------------------- 1 | export interface ShortcutCallbacks { 2 | prompt: () => void 3 | chat: () => void 4 | command: () => void 5 | readaloud: () => void 6 | transcribe: () => void 7 | scratchpad: () => void 8 | realtime: () => void 9 | studio: () => void 10 | } 11 | 12 | export type Application = { 13 | id: string 14 | name: string 15 | path: string 16 | window: string 17 | } 18 | 19 | export interface Automator { 20 | getForemostApp(): Promise 21 | selectAll(): Promise 22 | moveCaretBelow(): Promise 23 | copySelectedText(): Promise 24 | deleteSelectedText(): Promise 25 | pasteText(): Promise 26 | } 27 | 28 | export type CommandAction = 'default' | 'copy' | 'insert' | 'replace' 29 | 30 | export type Command = { 31 | id: string 32 | type: 'system' | 'user' 33 | icon: string 34 | label?: string 35 | action: 'chat_window' | 'paste_below' | 'paste_in_place' | 'clipboard_copy' 36 | template?: string 37 | shortcut: string 38 | state: 'enabled' | 'disabled' | 'deleted' 39 | engine: string 40 | model: string 41 | } 42 | 43 | export type RunCommandParams = { 44 | textId: string 45 | sourceApp: Application | null 46 | command: Command 47 | action: CommandAction 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/types/index.ts: -------------------------------------------------------------------------------- 1 | export const CHATBOT_DEFAULTS = { 2 | name: '', 3 | apiKey: '', 4 | apiCli: '', 5 | icon: '', 6 | url: 'https://api2.aiql.com', 7 | urlList: ['https://api2.aiql.com'], 8 | 9 | path: '/chat/completions', 10 | pathList: [ 11 | '/chat/completions', 12 | '/v1/chat/completions', 13 | '/v1/openai/chat/completions', 14 | '/openai/v1/chat/completions' 15 | ], 16 | 17 | model: 'Qwen/Qwen3-32B', 18 | modelList: ['Qwen/Qwen3-32B', 'Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo', 'openai/gpt-oss-120b'], 19 | 20 | authPrefix: 'Bearer', 21 | authPrefixList: ['Bearer', 'Base', 'Token'], 22 | 23 | maxTokensPrefixList: ['max_tokens', 'max_completion_tokens', 'max_new_tokens'], 24 | maxTokensPrefix: 'max_tokens', 25 | maxTokensValue: undefined, 26 | 27 | temperature: undefined, 28 | topP: undefined, 29 | method: 'POST', 30 | contentType: 'application/json', 31 | stream: true, 32 | reasoningEffort: undefined, 33 | enableThinking: undefined, 34 | authorization: true, 35 | mcp: true 36 | } 37 | 38 | export const REASONING_EFFORT = ['minimal', 'low', 'medium', 'high'] as const 39 | 40 | export type ReasoningEffort = (typeof REASONING_EFFORT)[number] 41 | 42 | export const ENABLE_THINKING = ['true', 'false'] 43 | -------------------------------------------------------------------------------- /src/renderer/store/dxt.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref, toRaw } from 'vue' 3 | import type { userConfigValue } from '@/types/mcp' 4 | import { useI18n } from 'vue-i18n' 5 | 6 | export const validateNumberRange = (min: number | undefined, max: number | undefined) => { 7 | const minNum = min ?? '-∞' 8 | const maxNum = max ?? '+∞' 9 | const { t } = useI18n() 10 | return t('dxt.number-range', { min: minNum, max: maxNum }) 11 | } 12 | 13 | export const useDxtStore = defineStore( 14 | 'dxtStore', 15 | () => { 16 | const configValues = ref>>({}) 17 | 18 | const updateConfigAttribute = (name: string, key: string, value: userConfigValue) => { 19 | if (!configValues.value[name]) { 20 | configValues.value[name] = {} 21 | } 22 | configValues.value[name][key] = value 23 | } 24 | 25 | const getConfig = (name: string) => { 26 | return toRaw(configValues.value[name]) ?? null 27 | } 28 | 29 | const getConfigAttribute = (name: string, key: string) => { 30 | return configValues.value[name]?.[key] ?? null 31 | } 32 | 33 | return { configValues, getConfig, updateConfigAttribute, getConfigAttribute } 34 | }, 35 | { 36 | persist: true 37 | } 38 | ) 39 | -------------------------------------------------------------------------------- /docs/src/zhHans/electron-how-to/main-and-renderer-process.md: -------------------------------------------------------------------------------- 1 | # 主流程与渲染器流程 2 | 3 | 一个**Electron**应用程序被分为代码,分为主进程和渲染器进程。 4 | 5 | **“主”**是`src/main`的代码,主要是由Electron处理的进程代码。**“渲染器”**是`src/renderer`的代码,主要用于前端渲染过程,如Vue。 6 | 7 | 一般来说,**Node.js**脚本无法在渲染器进程中运行。例如,包含Node.js使用的API的模块,或**Node.js**的本机模块,如`path`或`net`、`os`或`crypto`。 8 | 9 | 预加载脚本在渲染器加载之前运行。它为主进程创建了一个桥梁,出于安全考虑,将Node.js脚本的执行与渲染器区域分开并隔离。 10 | 11 | 为了安全执行脚本,建议主进程执行Node脚本,渲染器通过消息传递接收执行结果。这可以通过**IPC通信**来实现。 12 | 13 | 欲了解更多信息,请参阅以下文章: https://www.electronjs.org/docs/latest/tutorial/ipc 14 | 15 | ### 如何在渲染器上运行Node.js? 16 | 17 | 如果您想跳过安全问题并在渲染器中使用 Node.js 脚本,需要在 `vite.config.ts` 文件中将 `nodeIntegration` 设置为 `true`。 18 | 19 | ```javascript 20 | rendererPlugin({ 21 | nodeIntegration: true 22 | }) 23 | ``` 24 | 25 | 欲了解更多信息,请参阅以下文章: https://github.com/electron-vite/vite-plugin-electron-renderer 26 | 27 | ### 如何解决开发环境中的CORS限制问题? 28 | 29 | 默认情况下,WebSecurity功能(定义在`DEFAULT_WEB_PREFERENCES`)会启用生产级安全防护。但在后端开发/调试过程中: 30 | 31 | - 若出现以下情况可临时关闭webSecurity: 32 | - 后端未配置CORS响应头(如Access-Control-Allow-Origin等) 33 | 34 | - OPTIONS预检请求被302重定向(非标准处理方式) 35 | 36 | 此操作将允许直接向聊天补全API发起POST请求,绕过浏览器强制的CORS限制。 37 | 38 | > [!WARNING] 本方案仅限本地开发环境使用。部署至生产环境前务必重新启用webSecurity功能。 39 | 40 | 欲了解更多信息,请参阅以下文章: https://www.electronjs.org/zh/docs/latest/tutorial/security 41 | -------------------------------------------------------------------------------- /tests/specs/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, beforeAll, afterAll } from '../fixtures.mts' 2 | 3 | test.beforeAll(beforeAll) 4 | test.afterAll(afterAll) 5 | 6 | test('Document element check', async ({ page, util }) => { 7 | try { 8 | await expect( 9 | page.getByTestId('main-menu').first(), 10 | `Confirm main logo is visible` 11 | ).toBeVisible() 12 | await expect( 13 | page.getByTestId('select-language').first(), 14 | `Confirm language selector is visible` 15 | ).toBeVisible() 16 | 17 | await util.captureScreenshot(page, 'result') 18 | } catch (error) { 19 | throw await util.onTestError(error) 20 | } 21 | }) 22 | 23 | test('Counter button click check', async ({ page, util }) => { 24 | try { 25 | await page.getByTestId('btn-menu-mcp').click({ clickCount: 2, delay: 50 }) 26 | 27 | await page.getByTestId('btn-menu-chat').click({ clickCount: 2, delay: 50 }) 28 | 29 | await page.getByTestId('btn-menu-setting').click({ clickCount: 2, delay: 50 }) 30 | 31 | // const counterValueElement = await page 32 | // .getByTestId('counter-badge') 33 | // .getByRole('status') 34 | // .innerHTML() 35 | 36 | // expect(counterValueElement).toBe('10') 37 | } catch (error) { 38 | throw await util.onTestError(error) 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /.github/workflows/build-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Build Artifacts 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | name: Build ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest, macos-latest] 12 | runs-on: ${{ matrix.os }} 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: "22" 22 | 23 | - name: Install dependencies 24 | run: npm install 25 | 26 | - name: Build project 27 | run: npm run build:${{ fromJson('{"ubuntu-latest":"linux","macos-latest":"mac","windows-latest":"win"}')[matrix.os] }} 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Archive production artifacts 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: build-artifacts-${{ matrix.os }} 35 | path: | 36 | release/*/*.exe 37 | release/*/*.appx 38 | release/*/*.zip 39 | release/*/*.deb 40 | release/*/*.rpm 41 | release/*/*.snap 42 | release/*/*.dmg 43 | retention-days: 3 44 | compression-level: 0 -------------------------------------------------------------------------------- /docs/src/en/installation-and-build/automated-testing.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 4 3 | --- 4 | 5 | # Automated Testing 6 | 7 | **TUUI** includes automated testing. The testing framework uses Microsoft's [Playwright](https://playwright.dev). 8 | 9 | **Playwright** is optimized for web application testing and has full support for the **Electron** framework. It is simple to install, requires no configuration to start testing immediately, and is cross-platform. You can learn more about **Playwright** here: https://github.com/microsoft/playwright 10 | 11 | Only very simple launch and behavioral tests for the template main screen have been implemented in this template. Advanced testing will depend on the scope of your application. 12 | 13 | Currently, the test specification file is located in the `tests` directory and the test results file is located in `tests/results`. (The built-in test specification file does not generate a separate results file.) 14 | 15 | The Playwright configuration is `playwright.config.ts` in the project root, see the following documentation for more information on this: https://playwright.dev/docs/test-configuration 16 | 17 | Once everything is configured, you can run a test with the following command. 18 | 19 | ```shell 20 | $ npm run test 21 | ``` 22 | 23 | Before running the test, empty the build directory (`dist`) and compile the package for the test. 24 | -------------------------------------------------------------------------------- /src/renderer/store/locale.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { useI18n } from 'vue-i18n' 3 | 4 | type LocaleObjectType = { 5 | title: string 6 | value: string 7 | name: string 8 | src: string 9 | } 10 | 11 | interface LocaleStoreState { 12 | selected: string | undefined 13 | list: LocaleObjectType[] 14 | fallback: LocaleObjectType 15 | } 16 | 17 | export const useLocaleStore = defineStore('localeStore', { 18 | state: () => 19 | ({ 20 | selected: undefined, 21 | // Sourced from the 'flag-icons' npm package 22 | list: [ 23 | { title: 'English', value: 'en', name: 'united-states', src: 'flags/us.svg' }, 24 | { title: '简体中文', value: 'zh', name: 'china', src: 'flags/cn.svg' } 25 | ], 26 | fallback: { title: 'English', value: 'en', name: 'united-states', src: 'flags/us.svg' } 27 | }) as LocaleStoreState, 28 | getters: {}, 29 | persist: { 30 | include: ['selected'] 31 | }, 32 | actions: { 33 | getLocale() { 34 | const { locale } = useI18n() 35 | return locale.value 36 | }, 37 | change(lang: string) { 38 | const { locale } = useI18n() 39 | locale.value = lang 40 | }, 41 | getIcon() { 42 | const value = this.getLocale() 43 | const item = this.list.find((lang) => lang.value === value) || this.fallback 44 | return item.src 45 | } 46 | } 47 | }) 48 | -------------------------------------------------------------------------------- /src/renderer/store/snackbar.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | type SnackbarType = 'error' | 'success' | 'info' | 'warning' | 'unknown' 4 | 5 | interface SnackbarState { 6 | isShow: boolean 7 | message: string 8 | type: SnackbarType 9 | } 10 | 11 | export const useSnackbarStore = defineStore('snackbarStore', { 12 | state: () => 13 | ({ 14 | isShow: false, 15 | message: '', 16 | type: 'unknown' 17 | }) as SnackbarState, 18 | 19 | actions: { 20 | showMessage(message: string, type: SnackbarType = 'unknown') { 21 | console.log(message) 22 | this.isShow = true 23 | this.message = message 24 | this.type = type 25 | }, 26 | 27 | showErrorMessage(message: string) { 28 | this.showMessage(message, 'error') 29 | }, 30 | showSuccessMessage(message: string) { 31 | this.showMessage(message, 'success') 32 | }, 33 | showInfoMessage(message: string) { 34 | this.showMessage(message, 'info') 35 | }, 36 | showWarningMessage(message: string) { 37 | this.showMessage(message, 'warning') 38 | }, 39 | getIcon() { 40 | const icon = { 41 | info: 'mdi-information', 42 | success: 'mdi-check-circle', 43 | error: 'mdi-alert-circle', 44 | warning: 'mdi-alert', 45 | unknown: 'mdi-help-circle-outline' 46 | } 47 | 48 | return icon[this.type] 49 | } 50 | } 51 | }) 52 | -------------------------------------------------------------------------------- /src/main/utils/notification.ts: -------------------------------------------------------------------------------- 1 | import { Notification } from 'electron' 2 | import Constants from './Constants' 3 | 4 | export interface NotificationOptions { 5 | title?: string 6 | body: string 7 | silent?: boolean 8 | icon?: string 9 | } 10 | 11 | export interface NotificationEvents { 12 | onClick?: () => void 13 | onShow?: () => void 14 | onClose?: () => void 15 | } 16 | 17 | export function showNotification( 18 | options: NotificationOptions, 19 | events?: NotificationEvents 20 | ): boolean { 21 | if (!Notification.isSupported()) { 22 | return false 23 | } 24 | 25 | try { 26 | const notification = new Notification({ 27 | title: options.title ?? Constants.APP_NAME, 28 | body: options.body, 29 | silent: options.silent ?? false, 30 | icon: options.icon ?? Constants.ASSETS_PATH.icon_raw 31 | }) 32 | 33 | if (events?.onClick) { 34 | notification.on('click', events.onClick) 35 | } 36 | 37 | if (events?.onShow) { 38 | notification.on('show', events.onShow) 39 | } 40 | 41 | if (events?.onClose) { 42 | notification.on('close', events.onClose) 43 | } 44 | 45 | notification.show() 46 | return true 47 | } catch (error) { 48 | console.error('Failed to show notification:', error) 49 | return false 50 | } 51 | } 52 | 53 | export function isNotificationSupported(): boolean { 54 | return Notification.isSupported() 55 | } 56 | -------------------------------------------------------------------------------- /docs/src/zhHans/installation-and-build/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 1 3 | --- 4 | 5 | # 入门 6 | 7 | ## 克隆项目 8 | 9 | 使用以下命令克隆该 repo。此方法适用于直接向 TUUI 代码库投稿。 10 | 11 | ```shell 12 | $ git clone https://github.com/AI-QL/tuui 13 | ``` 14 | 15 | ## 安装 16 | 17 | 克隆项目后,在终端运行以下命令: 18 | 19 | ```shell 20 | # via npm 21 | $ npm i 22 | 23 | # via yarn (https://yarnpkg.com) 24 | $ yarn install 25 | 26 | # via pnpm (https://pnpm.io) 27 | $ pnpm i 28 | ``` 29 | 30 | ## 在开发环境中运行 31 | 32 | 开发环境中的应用程序通过 **[Vite](https://vitejs.dev)** 运行。 33 | 34 | ```shell 35 | $ npm run dev 36 | ``` 37 | 38 | 如果运行命令行命令后应用程序没有出现,您可能需要检查默认端口是否被其他应用程序使用。 39 | 40 | Vite 默认使用端口 `5173`。 41 | 42 | ## NPM 包更新指南 43 | 44 | 1. 检查过时的包: 45 | 46 | ```shell 47 | $ npm outdated 48 | ``` 49 | 50 | 2. 使用 `npm-check-updates` 检查可用更新: 51 | 52 | ```shell 53 | $ npx npm-check-updates 54 | ``` 55 | 56 | 3. 更新 `package.json` 文件: 57 | 58 | ```shell 59 | $ npx npm-check-updates -u 60 | ``` 61 | 62 | 4. 安装新版本: 63 | 64 | ```shell 65 | $ npm install 66 | ``` 67 | 68 | **温馨提示**:完成重要版本更新后,请务必测试您的应用以确保兼容性。 69 | 70 | 5. (可选) 启用 NPM 镜像 registry 71 | 72 | 在某些受限地区,可能无法通过默认 registry 下载 Electron。此时可将 registry 切换至 Electron 镜像下载源(例如 `npmmirror.com`)。 73 | 74 | > `npmrc.template` 文件中提供了示例配置,将其重命名为 `.npmrc` 即可使用镜像源。 75 | 76 | ## NPM 版本更新 77 | 78 | 你可以通过以下方式更新项目版本信息: 79 | 80 | ```shell 81 | $ npm version --no-git-tag-version 82 | ``` 83 | 84 | **温馨提示**: 请同时更新项目目录和 docs 目录中的版本信息 85 | -------------------------------------------------------------------------------- /src/main/mcp/connection.ts: -------------------------------------------------------------------------------- 1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js' 2 | import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' 3 | 4 | export const validLogLevels = ['trace', 'debug', 'info', 'warn', 'error'] as const 5 | 6 | export type LogLevel = (typeof validLogLevels)[number] 7 | 8 | export async function connect(client: Client, transport: Transport): Promise { 9 | try { 10 | await client.connect(transport) 11 | } catch (error) { 12 | try { 13 | await disconnect(transport) 14 | } catch (_disconnectError) {} 15 | throw new Error( 16 | `Failed to connect to MCP server: ${error instanceof Error ? error.message : String(error)}` 17 | ) 18 | } 19 | } 20 | 21 | export async function disconnect(transport: Transport): Promise { 22 | try { 23 | await transport.close() 24 | } catch (error) { 25 | throw new Error( 26 | `Failed to disconnect from MCP server: ${error instanceof Error ? error.message : String(error)}` 27 | ) 28 | } 29 | } 30 | 31 | // Set logging level 32 | export async function setLoggingLevel( 33 | client: Client, 34 | level: LogLevel 35 | ): Promise> { 36 | try { 37 | const response = await client.setLoggingLevel(level as any) 38 | return response 39 | } catch (error) { 40 | throw new Error( 41 | `Failed to set logging level: ${error instanceof Error ? error.message : String(error)}` 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createPinia } from 'pinia' 3 | import { createStatePersistence } from 'pinia-plugin-state-persistence' 4 | 5 | import App from '@/renderer/App.vue' 6 | import router from '@/renderer/router' 7 | import vuetify from '@/renderer/plugins/vuetify' 8 | import i18n from '@/renderer/plugins/i18n' 9 | import Vue3Lottie from 'vue3-lottie' 10 | 11 | import type { MCPAPI, DXTAPI } from '@/types/mcp' 12 | import type { LlmConfig } from '@/types/llm' 13 | import type { PopupConfig } from '@/types/popup' 14 | import type { StartupConfig } from '@/types/startup' 15 | 16 | // Add API key defined in contextBridge to window object type 17 | declare global { 18 | // eslint-disable-next-line no-unused-vars 19 | interface Window { 20 | mainApi?: any 21 | llmApis?: { 22 | get: () => LlmConfig 23 | } 24 | popupApis?: { 25 | get: () => PopupConfig 26 | } 27 | startupApis?: { 28 | get: () => StartupConfig 29 | } 30 | mcpServers?: { 31 | get: () => MCPAPI 32 | refresh: () => Promise<{}> 33 | update: (_name: string) => Promise<{}> 34 | } 35 | dxtManifest?: { 36 | get: () => DXTAPI 37 | refresh: () => Promise<{}> 38 | update: (_name: string) => Promise<{}> 39 | } 40 | } 41 | } 42 | 43 | const app = createApp(App) 44 | const pinia = createPinia() 45 | pinia.use(createStatePersistence()) 46 | 47 | app.use(vuetify).use(i18n).use(router).use(pinia).use(Vue3Lottie) 48 | 49 | app.mount('#app') 50 | -------------------------------------------------------------------------------- /src/renderer/screens/agent/AgentSideDrawer.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/renderer/components/common/MarkdownCard.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 59 | 60 | 66 | -------------------------------------------------------------------------------- /src/types/ipc.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SamplingRequest, 3 | SamplingResponse, 4 | ElicitRequest, 5 | ElicitResponse, 6 | CommandRequest, 7 | CommandResponse, 8 | McpInitResponse 9 | } from '@/main/types' 10 | 11 | import { McpProgressCallbackObject } from '@/main/mcp/types' 12 | 13 | import { SamplingMessage } from '@modelcontextprotocol/sdk/types' 14 | 15 | export type { 16 | SamplingRequest, 17 | SamplingResponse, 18 | SamplingMessage, 19 | ElicitRequest, 20 | ElicitResponse, 21 | CommandResponse, 22 | McpInitResponse 23 | } 24 | 25 | export type SamplingRequestParams = SamplingRequest['params'] 26 | 27 | export type IpcSamplingRequest = { 28 | request: SamplingRequest 29 | responseChannel: string 30 | } 31 | 32 | export type IpcSamplingRequestCallback = (_event: Event, _progress: IpcSamplingRequest) => void 33 | 34 | export type IpcElicitRequest = { 35 | request: ElicitRequest 36 | responseChannel: string 37 | } 38 | 39 | export type IpcElicitRequestCallback = (_event: Event, _progress: IpcElicitRequest) => void 40 | 41 | export type IpcCommandRequest = { 42 | request: CommandRequest 43 | } 44 | 45 | export type IpcCommandRequestCallback = (_event: Event, _progress: IpcCommandRequest) => void 46 | 47 | export type IpcMcpInitRequest = { 48 | callback: McpProgressCallbackObject 49 | } 50 | 51 | export type IpcMcpInitRequestCallback = (_event: Event, _progress: IpcMcpInitRequest) => void 52 | 53 | export type IpcFileTransferRequest = { name: string; data: ArrayBuffer } 54 | 55 | export type IpcFileTransferResponse = 56 | | { name: string; success: false; reason: string } 57 | | { name; success: true; path: string } 58 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | 22 | 67 | -------------------------------------------------------------------------------- /src/main/aid/window.ts: -------------------------------------------------------------------------------- 1 | import { dialog, shell } from 'electron' 2 | 3 | import { BrowserWindowConstructorOptions } from 'electron' 4 | import { anyDict } from './utils' 5 | import { Application } from './types' 6 | 7 | // export * from './windows/index'; 8 | // export * from './windows/main'; 9 | // export * from './windows/anywhere'; 10 | export * from './commands' 11 | // export * from './windows/settings'; 12 | // export * from './windows/readaloud'; 13 | // export * from './windows/realtime'; 14 | // export * from './windows/transcribe'; 15 | // export * from './windows/scratchpad'; 16 | // export * from './windows/computer'; 17 | // export * from './windows/studio'; 18 | // export * from './windows/debug'; 19 | 20 | export type BrowserWindowCommandOptions = { 21 | showInDock?: boolean 22 | keepHidden?: boolean 23 | queryParams?: anyDict 24 | hash?: string 25 | } 26 | 27 | export type CreateWindowOpts = BrowserWindowConstructorOptions & BrowserWindowCommandOptions 28 | 29 | export type ReleaseFocusOpts = { 30 | sourceApp?: Application 31 | delay?: number 32 | } 33 | 34 | export const showMasLimitsDialog = () => { 35 | const version = 36 | process.arch === 'arm64' ? 'Apple Silicon (M1, M2, M3, M4)' : 'Mac Intel architecture' 37 | const response = dialog.showMessageBoxSync(null, { 38 | message: 39 | 'This feature (and many others) are not available on the Mac App Store version. You may want to check the version from the website.', 40 | detail: `You will need to download the "${version}" version.`, 41 | buttons: ['Close', 'Check website'], 42 | defaultId: 1 43 | }) 44 | if (response === 1) { 45 | shell.openExternal(`https://github.com/AI-QL/tuui/releases`) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/screens/mcp/McpCentralStage.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 43 | -------------------------------------------------------------------------------- /docs/src/zhHans/installation-and-build/npm-scripts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NPM Scripts 3 | order: 3 4 | --- 5 | 6 | # Npm 脚本 7 | 8 | > $ npm run %SCRIPT_NAME% 9 | 10 | ## 一般情况 11 | 12 | | 脚本名称 | 说明 | 13 | | ----------------- | ---------------------------------------------------------- | 14 | | `dev` | 启动电子作为开发环境 | 15 | | `dev:debug` | 将 Electron 作为开发环境启动(使用 vite debug) | 16 | | `dev:debug:force` | 以Electron作为开发环境启动(使用vite调试+清理vite缓存) | 17 | | `build:pre` | 通常在编译时运行的命令。此脚本无需单独运行。 | 18 | | `build` | 为当前操作系统打包。 | 19 | | `build:all` | 为整个操作系统构建指定软件包(需要跨平台构建配置) | 20 | | `build:dir` | `electron-builder`目录构建 | 21 | | `build:mac` | 为macOS构建预配置软件包 | 22 | | `build:linux` | 为Linux构建预配置软件包 | 23 | | `build:win` | 为Windows构建预配置软件包 | 24 | | `lint` | ESLint代码检查。它不会修改代码。 | 25 | | `lint:fix` | ESLint代码检查。使用自动修复功能修复代码。 | 26 | | `format` | 更漂亮的代码检查。它不会修改代码。 | 27 | | `format:fix` | 更漂亮的代码检查。使用自动修复功能修复代码。 | 28 | | `test` | 根据测试规范文件构建测试包并运行测试。 | 29 | | `test:linux` | 根据测试规范文件构建测试包并运行测试。(仅适用于linux ci) | 30 | 31 | ## 文档 32 | 33 | 仅用于为项目文档提供素材。必须从“文档”目录位置运行。 34 | 35 | | Script Name | Description | 36 | | ----------- | ---------------------------------------------- | 37 | | `dev` | 启动本地文档服务器。(开发中) | 38 | | `build` | 构建本地文档服务器。仅用于 GitHub 页面构建器。 | 39 | | `serve` | 启动本地文档服务器。 | 40 | -------------------------------------------------------------------------------- /src/renderer/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import { createVuetify } from 'vuetify' 2 | import { ko, en, zhHans, zhHant, de, es, ja, fr, ru, pt, nl } from 'vuetify/locale' 3 | import { VFileUpload } from 'vuetify/labs/VFileUpload' 4 | import { VIconBtn } from 'vuetify/labs/VIconBtn' 5 | import { aliases, mdi } from 'vuetify/iconsets/mdi' 6 | import 'vuetify/styles' 7 | import '@mdi/font/css/materialdesignicons.min.css' 8 | 9 | import colors from 'vuetify/lib/util/colors' 10 | 11 | export default createVuetify({ 12 | locale: { 13 | messages: { ko, en, zhHans, zhHant, de, es, ja, fr, ru, pt, nl }, 14 | locale: 'en', 15 | fallback: 'en' 16 | }, 17 | icons: { 18 | defaultSet: 'mdi', 19 | aliases, 20 | sets: { 21 | mdi 22 | } 23 | }, 24 | components: { 25 | VFileUpload, 26 | VIconBtn 27 | }, 28 | theme: { 29 | themes: { 30 | light: { 31 | dark: false, 32 | colors: { 33 | primary: '#344767', 34 | background: '#FFFFFF', 35 | surface: '#FFFFFF', 36 | secondary: colors.lightBlue.darken4, // #01579B '#2B323B', 37 | alert: colors.brown.lighten1, 38 | pending: colors.yellow.base, 39 | 'grey-0': '#F2F5F8', 40 | 'grey-50': '#FAFAFA', 41 | 'grey-100': '#F5F5F5', 42 | 'grey-200': '#EEEEEE', 43 | 'grey-300': '#E0E0E0', 44 | 'grey-400': '#BDBDBD', 45 | 'grey-500': '#9E9E9E', 46 | 'grey-600': '#757575', 47 | 'grey-700': '#616161', 48 | 'grey-800': '#424242', 49 | 'grey-900': '#212121' 50 | } 51 | }, 52 | dark: { 53 | dark: true, 54 | colors: { 55 | primary: colors.indigo.darken2, // #303F9F 56 | secondary: colors.indigo.darken4 // #1A237E 57 | } 58 | } 59 | } 60 | } 61 | }) 62 | -------------------------------------------------------------------------------- /src/renderer/screens/chat/ChatHistoryScreen.vue: -------------------------------------------------------------------------------- 1 | 31 | 58 | -------------------------------------------------------------------------------- /src/renderer/components/common/LocaleBtn.vue: -------------------------------------------------------------------------------- 1 | 20 | 53 | 54 | 60 | -------------------------------------------------------------------------------- /docs/src/en/electron-how-to/preload-script.md: -------------------------------------------------------------------------------- 1 | # Preload Script 2 | 3 | The preload script in Electron.js is a secure area designed for communication between the main and renderer processes. It is typically used for **[IPC communication](https://www.electronjs.org/docs/latest/tutorial/ipc)**. 4 | 5 | For more information, see the following articles https://www.electronjs.org/docs/latest/tutorial/tutorial-preload 6 | 7 | For compatibility and security with the latest version of Electron, we do not recommend using the old `electron/remote` module. If you want to utilize system events or Node scripts, it is recommended to do so in the main process, not the renderer. 8 | 9 | TUUI's preload script is located in the `src/preload` folder. To create a new IPC communication channel, add the channel name to the following variable to whitelist it for communication. 10 | 11 | - `mainAvailChannels`: Send an event from main to renderer. (`window.mainApi.send('channelName')`) 12 | - `rendererAvailChannels`: Send an event from renderer to main. (`mainWindow.webContents.send('channelName')`) 13 | 14 | When sending events from renderer to main, you access the `window.mainApi` object instead of `ipcRenderer.send`. The `mainApi` is a placeholder name provided in the template for demonstration purposes only. It should not be used in production implementations. For actual integrations like **MCP servers**, use the dedicated `mcpServers` API instead. 15 | 16 | Here are the supported functions for mainApi: 17 | 18 | - `send`: Send an event to main. 19 | - `on`: A listener to receive events sent by main. 20 | - `once`: A listener to receive events sent by main. (Handle only one call) 21 | - `off`: Remove an event listener 22 | - `invoke`: Functions that can send events to main and receive data asynchronously. 23 | 24 | To change and modify `mainApi`, you need to modify `exposeInMainWorld` in `src/preload/index.ts`. 25 | -------------------------------------------------------------------------------- /src/main/assets/config/popup.json: -------------------------------------------------------------------------------- 1 | { 2 | "prompts": [ 3 | { 4 | "icon": "mdi-alphabet-latin", 5 | "title": "To English", 6 | "prompt": "You are an English translator, and you need to translate the text I provide into English, retaining the original format as much as possible. The translation should be directly ready for external presentation, without adding greetings, farewells, or any extra comments or explanations. For proper nouns, you may keep the original term in parentheses right after the English translation. \n---" 7 | }, 8 | { 9 | "icon": "mdi-ideogram-cjk-variant", 10 | "title": "翻译成中文", 11 | "prompt": "你是一名中文翻译,你需要尽量以原有格式将我提供的文本翻译成中文。翻译内容应当能够被直接用于对外呈现,不能添加问候语、告别语,不要额外的评论或解释。对于专有名词可以在对应的中文翻译旁的括号内保留外文原词。 \n---" 12 | }, 13 | { 14 | "icon": "mdi-ab-testing", 15 | "title": "Imporve English", 16 | "prompt": "You are a technical expert and are preparing an English speech. You need to refine the various text snippets provided below into formal English expressions. When translating, consider the audience's level of understanding, and try to avoid using obscure English words. The translation should be directly suitable for external presentation, without adding greetings, farewells, or any extra comments or explanations. \n---" 17 | }, 18 | { 19 | "icon": "mdi-language-typescript", 20 | "title": "解决TypeScript问题", 21 | "prompt": "你是一个ts代码助手,帮助解决typescript以及前端项目中遇到的问题。你的回答需要尽量使用最新的解决方案和风格,比如对于vue,请使用vue3 script setup。 \n---" 22 | }, 23 | { 24 | "icon": "mdi-language-python", 25 | "title": "解决Python问题", 26 | "prompt": "你是一个py代码助手,帮助解决python以及AI项目中遇到的问题。 \n---" 27 | }, 28 | { 29 | "icon": "mdi-git", 30 | "title": "Generate Commit Message", 31 | "prompt": "You are a code reviewer, you need to generate a short commit message according to provided git diff. \n---" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /src/renderer/components/common/ConfigJsonCard.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 75 | -------------------------------------------------------------------------------- /src/main/mcp/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ListToolsResultSchema, 3 | CallToolResultSchema, 4 | ListPromptsResultSchema, 5 | GetPromptResultSchema, 6 | ListResourcesResultSchema, 7 | ReadResourceResultSchema, 8 | ListResourceTemplatesResultSchema 9 | } from '@modelcontextprotocol/sdk/types.js' 10 | 11 | import { Client } from '@modelcontextprotocol/sdk/client/index.js' 12 | 13 | import { 14 | StdioClientTransport, 15 | StdioServerParameters 16 | } from '@modelcontextprotocol/sdk/client/stdio.js' 17 | 18 | import { McpMetadata, McpServerDescription } from '@/types/mcp' 19 | 20 | export const McpServerCapabilitySchemas = { 21 | tools: { 22 | list: ListToolsResultSchema, 23 | call: CallToolResultSchema 24 | }, 25 | prompts: { 26 | list: ListPromptsResultSchema, 27 | get: GetPromptResultSchema 28 | }, 29 | resources: { 30 | list: ListResourcesResultSchema, 31 | read: ReadResourceResultSchema, 32 | 'templates/list': ListResourceTemplatesResultSchema 33 | } 34 | } 35 | 36 | export type McpServerConfig = StdioServerParameters 37 | 38 | export interface McpClientTransport { 39 | client: Client 40 | transport: StdioClientTransport 41 | } 42 | 43 | export type McpProgressCallbackObject = { 44 | name: string 45 | message: string 46 | status: 'pending' | 'error' | 'success' 47 | } 48 | 49 | export type McpProgressCallback = ( 50 | ..._args: [ 51 | McpProgressCallbackObject['name'], 52 | McpProgressCallbackObject['message'], 53 | McpProgressCallbackObject['status'] 54 | ] 55 | ) => void 56 | 57 | export type McpMetadataConfig = { 58 | [key: string]: McpMetadata 59 | } 60 | 61 | export type McpClientObject = { 62 | name: string 63 | configJson?: McpServerConfig 64 | connection?: McpClientTransport 65 | } 66 | 67 | export type McpFeatureObject = { 68 | name: string 69 | config: McpClientObject['configJson'] 70 | description?: McpServerDescription 71 | } 72 | -------------------------------------------------------------------------------- /.github/workflows/playwright-test.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - '**' 8 | - '!LICENSE' 9 | - '!*.md' 10 | - '!docs/**' 11 | - '!.github/**' 12 | - '.github/workflows/playwright-test.yml' 13 | pull_request: 14 | branches: [main] 15 | workflow_dispatch: 16 | 17 | jobs: 18 | playwright-test: 19 | runs-on: ${{ matrix.os }} 20 | name: Test Node.js ${{ matrix.node_version }} on ${{ matrix.os }} 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | node_version: ['20', '22'] 25 | os: [windows-latest, macos-latest, ubuntu-latest] 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | 32 | - name: Install xvfb 33 | if: runner.os == 'Linux' 34 | run: | 35 | sudo apt install -y -q --no-install-recommends xvfb 36 | 37 | - name: Setup Node.js ${{ matrix.node_version }} 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: ${{ matrix.node_version }} 41 | cache: npm 42 | cache-dependency-path: '**/package-lock.json' 43 | 44 | - name: Cache dependencies 45 | uses: actions/cache@v4 46 | id: npm-cache 47 | with: 48 | path: | 49 | **/node_modules 50 | key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} 51 | restore-keys: | 52 | ${{ runner.os }}-npm- 53 | 54 | - name: Install dependencies 55 | if: steps.npm-cache.outputs.cache-hit != 'true' 56 | run: npm i 57 | 58 | - name: Test module script (Windows or macOS) 59 | if: runner.os != 'Linux' 60 | run: | 61 | npm run test 62 | 63 | - name: Test module script (Linux) 64 | if: runner.os == 'Linux' 65 | run: | 66 | npm run test:linux 67 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, globalIgnores } from 'eslint/config' 2 | import vue from 'eslint-plugin-vue' 3 | import globals from 'globals' 4 | import parser from 'vue-eslint-parser' 5 | import path from 'node:path' 6 | import { fileURLToPath } from 'node:url' 7 | import js from '@eslint/js' 8 | import { FlatCompat } from '@eslint/eslintrc' 9 | 10 | const __filename = fileURLToPath(import.meta.url) 11 | const __dirname = path.dirname(__filename) 12 | const compat = new FlatCompat({ 13 | baseDirectory: __dirname, 14 | recommendedConfig: js.configs.recommended, 15 | allConfig: js.configs.all 16 | }) 17 | 18 | export default defineConfig([ 19 | globalIgnores([ 20 | '**/node_modules/', 21 | 'buildAssets/icons/', 22 | '**/dist/', 23 | '**/release/', 24 | '**/mcpb/', 25 | '**/.idea/', 26 | '**/.vscode/', 27 | '**/.github/' 28 | ]), 29 | { 30 | extends: compat.extends('plugin:vue/recommended', 'prettier'), 31 | 32 | plugins: { 33 | vue 34 | }, 35 | 36 | languageOptions: { 37 | globals: { 38 | ...globals.node, 39 | __static: true 40 | }, 41 | 42 | parser: parser, 43 | ecmaVersion: 2022, 44 | sourceType: 'module', 45 | 46 | parserOptions: { 47 | parser: '@typescript-eslint/parser', 48 | 49 | ecmaFeatures: { 50 | jsx: true 51 | } 52 | } 53 | }, 54 | 55 | rules: { 56 | 'arrow-parens': 0, 57 | 'generator-star-spacing': 0, 58 | 'no-case-declarations': 0, 59 | 'array-callback-return': 0, 60 | 'no-trailing-spaces': 1, 61 | 'no-control-regex': 0, 62 | 'no-useless-constructor': 0, 63 | 'no-useless-assignment': 0, 64 | 'no-useless-escape': 1, 65 | 'no-unused-vars': [ 66 | 'error', 67 | { 68 | argsIgnorePattern: '^_', 69 | varsIgnorePattern: '^_', 70 | caughtErrorsIgnorePattern: '^_' 71 | } 72 | ], 73 | 'node/no-deprecated-api': 0 74 | } 75 | } 76 | ]) 77 | -------------------------------------------------------------------------------- /src/renderer/screens/mcp/McpSideDrawer.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/renderer/screens/index.ts: -------------------------------------------------------------------------------- 1 | import ErrorScreen from '@/renderer/screens/ErrorScreen.vue' 2 | // import MainScreen from '@/renderer/screens/MainScreen.vue' 3 | 4 | import McpCentralStage from '@/renderer/screens/mcp/McpCentralStage.vue' 5 | import McpSideDrawer from '@/renderer/screens/mcp/McpSideDrawer.vue' 6 | import McpSideDock from '@/renderer/screens/mcp/McpSideDock.vue' 7 | 8 | import ChatMainScreen from '@/renderer/screens/chat/ChatScreen.vue' 9 | import ChatHistoryScreen from '@/renderer/screens/chat/ChatHistoryScreen.vue' 10 | import ChatInputScreen from '@/renderer/screens/chat/ChatInputScreen.vue' 11 | import ChatEndScreen from '@/renderer/screens/chat/ChatEndScreen.vue' 12 | 13 | import AgentCentralStage from '@/renderer/screens/agent/AgentCentralStage.vue' 14 | import AgentSideDock from '@/renderer/screens/agent/AgentSideDock.vue' 15 | import AgentSideDrawer from '@/renderer/screens/agent/AgentSideDrawer.vue' 16 | 17 | import SettingMainScreen from '@/renderer/screens/setting/SettingScreen.vue' 18 | import SettingDrawerScreen from '@/renderer/screens/setting/SettingMenuScreen.vue' 19 | import SettingConfigScreen from '@/renderer/screens/setting/SettingEndScreen.vue' 20 | 21 | export type ComponentName = 'centralStage' | 'sideDrawer' | 'sideDock' | 'bottomConsole' 22 | 23 | type ScreenType = { 24 | [key in ComponentName as `${key}`]?: any 25 | } 26 | 27 | export { ErrorScreen } 28 | 29 | export const McpScreen: ScreenType = { 30 | centralStage: McpCentralStage, 31 | sideDrawer: McpSideDrawer, 32 | sideDock: McpSideDock 33 | } 34 | 35 | export const ChatScreen: ScreenType = { 36 | centralStage: ChatMainScreen, 37 | sideDrawer: ChatHistoryScreen, 38 | sideDock: ChatEndScreen, 39 | bottomConsole: ChatInputScreen 40 | } 41 | 42 | export const AgentScreen: ScreenType = { 43 | centralStage: AgentCentralStage, 44 | sideDrawer: AgentSideDrawer, 45 | sideDock: AgentSideDock 46 | } 47 | 48 | export const SettingScreen: ScreenType = { 49 | centralStage: SettingMainScreen, 50 | sideDrawer: SettingDrawerScreen, 51 | sideDock: SettingConfigScreen 52 | } 53 | -------------------------------------------------------------------------------- /src/main/aid/nut.ts: -------------------------------------------------------------------------------- 1 | import { Application, Automator } from './types' 2 | import { wait } from './utils' 3 | 4 | let nut: any = undefined 5 | 6 | export default class NutAutomator implements Automator { 7 | nut() { 8 | return nut 9 | } 10 | 11 | protected async setup() { 12 | if (nut) { 13 | return true 14 | } 15 | if (nut === null) { 16 | return false 17 | } 18 | try { 19 | const nutPackage = '@nut-tree-fork/nut-js' 20 | nut = await import(nutPackage) 21 | return true 22 | } catch { 23 | console.log('Error loading nutjs. Automation not available.') 24 | nut = null 25 | return false 26 | } 27 | } 28 | 29 | async getForemostApp(): Promise { 30 | console.warn('getForemostApp not implemented (expected)') 31 | return null 32 | } 33 | 34 | async selectAll() { 35 | if (!(await this.setup())) throw new Error('nutjs not loaded') 36 | await nut.keyboard.type(this.commandKey(), nut.Key.A) 37 | await wait(this.delay()) 38 | } 39 | 40 | async moveCaretBelow() { 41 | if (!(await this.setup())) throw new Error('nutjs not loaded') 42 | await nut.keyboard.type(nut.Key.Down) 43 | await nut.keyboard.type(nut.Key.Enter) 44 | await nut.keyboard.type(nut.Key.Enter) 45 | await wait(this.delay()) 46 | } 47 | 48 | async copySelectedText() { 49 | if (!(await this.setup())) throw new Error('nutjs not loaded') 50 | await nut.keyboard.type(this.commandKey(), nut.Key.C) 51 | await wait(this.delay()) 52 | } 53 | 54 | async deleteSelectedText() { 55 | if (!(await this.setup())) throw new Error('nutjs not loaded') 56 | await nut.keyboard.type(nut.Key.Delete) 57 | await wait(this.delay()) 58 | } 59 | 60 | async pasteText() { 61 | if (!(await this.setup())) throw new Error('nutjs not loaded') 62 | await nut.keyboard.type(this.commandKey(), nut.Key.V) 63 | await wait(this.delay()) 64 | } 65 | 66 | protected delay() { 67 | return 150 68 | } 69 | 70 | protected commandKey() { 71 | return nut.Key.LeftControl 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /docs/src/en/installation-and-build/npm-scripts.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NPM Scripts 3 | order: 3 4 | --- 5 | 6 | # Npm Scripts 7 | 8 | > $ npm run %SCRIPT_NAME% 9 | 10 | ## General 11 | 12 | | Script Name | Description | 13 | | --- | --- | 14 | | `dev` | Start Electron as a development environment | 15 | | `dev:debug` | Start Electron as a development environment (with vite debug) | 16 | | `dev:debug:force` | Start Electron as a development environment (with vite debug + clean vite cache) | 17 | | `build:pre` | Commands commonly run at build time. This script does not need to be run separately. | 18 | | `build` | Build the package for the current operating system. | 19 | | `build:all` | Build a specified package for the entire operating system (Requires cross-platform build configuration) | 20 | | `build:dir` | `electron-builder` directory build | 21 | | `build:mac` | Build preconfigured packages for macOS | 22 | | `build:linux` | Build preconfigured packages for Linux | 23 | | `build:win` | Build preconfigured packages for Windows | 24 | | `lint` | ESLint code inspection. It does not modify the code. | 25 | | `lint:fix` | ESLint code inspection. Use auto-fix to fix your code. | 26 | | `format` | Prettier code inspection. It does not modify the code. | 27 | | `format:fix` | Prettier code inspection. Use auto-fix to fix your code. | 28 | | `test` | Build a package for testing and run tests against the test specification file. | 29 | | `test:linux` | Build a package for testing and run tests against the test specification file. (for linux ci only) | 30 | 31 | ## For Documentation 32 | 33 | Used only for contributing to project documentation. Must be run from the `docs` directory location. 34 | 35 | | Script Name | Description | 36 | | ----------- | ------------------------------------------------------------------ | 37 | | `dev` | Start the local document server. (For development) | 38 | | `build` | Build a local document server. Used only for GitHub page builders. | 39 | | `serve` | Start the local document server. | 40 | -------------------------------------------------------------------------------- /docs/src/en/installation-and-build/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 1 3 | --- 4 | 5 | # Getting Started 6 | 7 | ## Clone project 8 | 9 | Clone this repo using below command. This method is suitable for direct contributions to the TUUI repository. 10 | 11 | ```shell 12 | $ git clone https://github.com/AI-QL/tuui 13 | ``` 14 | 15 | ## Installation 16 | 17 | After cloning the project, run the following command in the terminal: 18 | 19 | ```shell 20 | # via npm 21 | $ npm i 22 | 23 | # via yarn (https://yarnpkg.com) 24 | $ yarn install 25 | 26 | # via pnpm (https://pnpm.io) 27 | $ pnpm i 28 | ``` 29 | 30 | ## Run in development environment 31 | 32 | Applications in the development environment run through **[Vite](https://vitejs.dev)**. 33 | 34 | ```shell 35 | $ npm run dev 36 | ``` 37 | 38 | If your application doesn't appear after running command line commands, you may need to review if the default port is being used by another app. 39 | 40 | Vite uses port `5173` by default. 41 | 42 | ## Update NPM Packages 43 | 44 | 1. Check for outdated packages: 45 | 46 | ```shell 47 | $ npm outdated 48 | ``` 49 | 50 | 2. Check available updates using `npm-check-updates`: 51 | 52 | ```shell 53 | $ npx npm-check-updates 54 | ``` 55 | 56 | 3. Upgrade your `package.json`: 57 | 58 | ```shell 59 | $ npx npm-check-updates -u 60 | ``` 61 | 62 | 4. Install the new versions: 63 | 64 | ```shell 65 | $ npm install 66 | ``` 67 | 68 | **Pro Tip**: Always test your application after major updates to ensure compatibility. 69 | 70 | 5. (Optional) Enable the NPM mirror registry 71 | 72 | In some restricted regions, downloading Electron may not be possible through the default registry. In such cases, you can switch to a mirror registry (such as `npmmirror.com`) for Electron downloads. 73 | 74 | > An example configuration is provided in `npmrc.template`. You can rename this file to `.npmrc` to use the mirror. 75 | 76 | ## Update NPM Version 77 | 78 | You can update the version info by: 79 | 80 | ```shell 81 | $ npm version --no-git-tag-version 82 | ``` 83 | 84 | **Pro Tip**: Update the version info in both the project and docs directories. 85 | -------------------------------------------------------------------------------- /src/renderer/screens/setting/SettingEndScreen.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 'Bug report' 2 | description: Report a bug in this project. 3 | labels: ['bug'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Before creating an issue, please read the following: 9 | 10 | - Read the `README.md` file on the project page or the documentation file and compare your code to the intent of the project. 11 | - Search to see if the same issue already exists, and keep the title concise and accurate so that it's easy for others to understand and search for. 12 | - Please create a separate issue for each type of issue. 13 | - For modular projects, make sure you're using the latest version of the module. 14 | - Please be as detailed as possible and write in English so that we can handle your issue quickly. 15 | - type: textarea 16 | attributes: 17 | label: Describe the bug 18 | description: | 19 | For the issue you are experiencing, please describe in detail what you are seeing, the error message, and the impact of the issue. If you are able to reproduce the issue, please list the steps in order. You can attach an image or video if necessary. 20 | validations: 21 | required: true 22 | - type: textarea 23 | attributes: 24 | label: Expected behavior 25 | description: Describe how it should be handled when it's normal behavior or what needs to be fixed. 26 | validations: 27 | - type: input 28 | attributes: 29 | label: Your environment - System OS 30 | description: Please describe the full range of OSes you are experiencing the issue with, preferably including the version. 31 | placeholder: Windows 11, macOS 15.x, Linux Ubuntu 24.04... 32 | validations: 33 | - type: input 34 | attributes: 35 | label: Your environment - Node.js 36 | description: If relevant, please describe the version of Node.js you are currently using. 37 | placeholder: 20.19.4, 22.17.0, 24.4.0... 38 | validations: 39 | - type: input 40 | attributes: 41 | label: Your environment - Python 42 | description: If relevant, please describe the version of Python you are currently using. 43 | placeholder: 3.10, 3.11, 3.12... 44 | validations: 45 | -------------------------------------------------------------------------------- /src/renderer/router/index.ts: -------------------------------------------------------------------------------- 1 | import { McpScreen, ChatScreen, AgentScreen, SettingScreen } from '@/renderer/screens' 2 | import { createRouter, createWebHashHistory } from 'vue-router' 3 | import { DefaultLayout } from '@/renderer/components/layouts' 4 | 5 | export default createRouter({ 6 | history: createWebHashHistory(), 7 | routes: [ 8 | { 9 | path: '/popup', 10 | component: () => import('@/renderer/screens/PopupScreen.vue') 11 | }, 12 | 13 | { 14 | path: '/', 15 | component: DefaultLayout, 16 | children: [ 17 | { 18 | path: '', 19 | components: McpScreen, 20 | meta: { 21 | titleKey: 'title.main' 22 | } 23 | }, 24 | { 25 | path: 'chat', 26 | components: ChatScreen, 27 | meta: { 28 | titleKey: 'title.chat' 29 | } 30 | }, 31 | { 32 | path: 'agent', 33 | components: AgentScreen, 34 | meta: { 35 | titleKey: 'title.agent' 36 | } 37 | }, 38 | { 39 | path: 'setting', 40 | components: SettingScreen, 41 | meta: { 42 | titleKey: 'title.setting' 43 | } 44 | } 45 | ] 46 | }, 47 | // { 48 | // path: '/', 49 | // components: McpScreen, 50 | // meta: { 51 | // titleKey: 'title.main' 52 | // } 53 | // }, 54 | // { 55 | // path: '/chat', 56 | // components: ChatScreen, 57 | // meta: { 58 | // titleKey: 'title.chat' 59 | // } 60 | // }, 61 | // { 62 | // path: '/agent', 63 | // components: AgentScreen, 64 | // meta: { 65 | // titleKey: 'title.agent' 66 | // } 67 | // }, 68 | // { 69 | // path: '/setting', 70 | // components: SettingScreen, 71 | // meta: { 72 | // titleKey: 'title.setting' 73 | // } 74 | // }, 75 | { 76 | path: '/error', 77 | component: () => import('@/renderer/screens/ErrorScreen.vue'), 78 | meta: { 79 | titleKey: 'title.error' 80 | } 81 | }, 82 | { 83 | path: '/:pathMatch(.*)*', 84 | redirect: '/' 85 | } 86 | ] 87 | }) 88 | -------------------------------------------------------------------------------- /src/renderer/components/layouts/DefaultLayout.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 56 | 57 | 66 | -------------------------------------------------------------------------------- /docs/src/en/electron-how-to/main-and-renderer-process.md: -------------------------------------------------------------------------------- 1 | # Main vs Renderer Process 2 | 3 | A **Electron** application is divided into code into a Main process and a Renderer process. 4 | 5 | **"Main"** is the code of `src/main` and is mainly the process code handled by Electron. **"Renderer"** is the code of `src/renderer`, mainly for front-end rendering process like Vue. 6 | 7 | In general, **Node.js** scripts cannot be run in the renderer process. Examples include modules that contain APIs used by Node.js, or native modules of **Node.js** such as `path` or `net`, `os` or `crypto`. 8 | 9 | Preload scripts are run before the renderer is loaded. It creates a bridge to the main process to keep the execution of Node.js scripts in the renderer area separate and isolated for security reasons. 10 | 11 | For secure script execution, it is recommended that the main process executes the Node scripts, and the renderer receives the execution results via messaging. This can be implemented via **IPC communication**. 12 | 13 | For more information on this, see the following articles: https://www.electronjs.org/docs/latest/tutorial/ipc 14 | 15 | ### How to run Node.js on a renderer? 16 | 17 | If you want to skip the security issues and use Node.js scripts in your renderer, you need to set `nodeIntegration` to `true` in your `vite.config.ts` file. 18 | 19 | ```javascript 20 | rendererPlugin({ 21 | nodeIntegration: true 22 | }) 23 | ``` 24 | 25 | For more information on this, see the following articles: https://github.com/electron-vite/vite-plugin-electron-renderer 26 | 27 | ### How to handle CORS restrictions in development? 28 | 29 | WebSecurity is enabled by default (in `DEFAULT_WEB_PREFERENCES`) for production-grade security. However, during backend development/debugging: 30 | 31 | - Temporarily disable webSecurity if: 32 | - Backend lacks CORS headers (Access-Control-Allow-Origin, etc.) 33 | - Preflight OPTIONS requests receive 302 redirects (non-standard handling) 34 | 35 | This allows direct POST requests to chat completions API without browser-enforced CORS restrictions. 36 | 37 | > [!WARNING] Only use this workaround for local development. Please re-enable webSecurity before deploying to production environments. 38 | 39 | For more information on this, see the following articles: https://www.electronjs.org/docs/latest/tutorial/security 40 | -------------------------------------------------------------------------------- /tests/fixtures.mts: -------------------------------------------------------------------------------- 1 | import * as base from '@playwright/test' 2 | import { _electron as electron, Page, ElectronApplication } from 'playwright' 3 | import { join } from 'path' 4 | import { main } from '../package.json' 5 | import TestUtil from './testUtil.mjs' 6 | 7 | let appElectron: ElectronApplication 8 | let page: Page 9 | 10 | const __cwd = process.cwd() 11 | const __isCiProcess = process.env.CI === 'true' 12 | const __testPath = join(__cwd, 'tests') 13 | const __testResultPath = join(__testPath, 'results') 14 | const __testScreenshotPath = join(__testResultPath, 'screenshots') 15 | 16 | export const beforeAll = async () => { 17 | // Open Electron app from build directory 18 | appElectron = await electron.launch({ 19 | args: [ 20 | main, 21 | ...(__isCiProcess ? ['--no-sandbox'] : []), 22 | '--enable-logging', 23 | '--ignore-certificate-errors', 24 | '--ignore-ssl-errors', 25 | '--ignore-blocklist', 26 | '--ignore-gpu-blocklist' 27 | ], 28 | locale: 'en-US', 29 | colorScheme: 'light', 30 | env: { 31 | ...process.env, 32 | NODE_ENV: 'production' 33 | } 34 | }) 35 | page = await appElectron.firstWindow() 36 | 37 | await page.waitForEvent('load') 38 | 39 | page.on('console', console.log) 40 | page.on('pageerror', console.log) 41 | 42 | const evaluateResult = await appElectron.evaluate(async ({ app, BrowserWindow }) => { 43 | const currentWindow = BrowserWindow.getFocusedWindow() 44 | 45 | // Fix window position for testing 46 | currentWindow.setPosition(50, 50) 47 | currentWindow.setSize(1080, 560) 48 | 49 | return { 50 | packaged: app.isPackaged, 51 | dataPath: app.getPath('userData') 52 | } 53 | }) 54 | 55 | base.expect(evaluateResult.packaged, 'app is not packaged').toBe(false) 56 | } 57 | 58 | export const afterAll = async () => { 59 | await appElectron.close() 60 | } 61 | 62 | export const test = base.test.extend({ 63 | // eslint-disable-next-line no-empty-pattern 64 | page: async ({}, use) => { 65 | await use(page) 66 | }, 67 | util: async ({ page }, use, testInfo) => { 68 | await use(new TestUtil(page, testInfo, __testScreenshotPath)) 69 | } 70 | }) 71 | 72 | export const expect = base.expect 73 | 74 | export default { 75 | test, 76 | expect, 77 | beforeAll, 78 | afterAll 79 | } 80 | -------------------------------------------------------------------------------- /src/main/aid/shortcuts.ts: -------------------------------------------------------------------------------- 1 | import { globalShortcut } from 'electron' 2 | import { loadSettings, Shortcut, disabledShortcutKey } from './config' 3 | 4 | export interface ShortcutCallbacks { 5 | command: () => void 6 | } 7 | 8 | export const unregisterShortcuts = () => { 9 | console.info('Unregistering shortcuts') 10 | globalShortcut.unregisterAll() 11 | } 12 | 13 | export const registerShortcuts = (callbacks: ShortcutCallbacks): void => { 14 | // unregister 15 | unregisterShortcuts() 16 | 17 | // load the config 18 | const config = loadSettings() 19 | 20 | // now register 21 | console.info('Registering shortcuts') 22 | registerShortcut('command', config.shortcuts.command, callbacks.command) 23 | } 24 | 25 | const keyToAccelerator = (key: string): string => { 26 | if (key === '+') return 'Plus' 27 | if (key === '↑') return 'Up' 28 | if (key === '↓') return 'Down' 29 | if (key === '←') return 'Left' 30 | if (key === '→') return 'Right' 31 | if (key === 'NumpadAdd') return 'numadd' 32 | if (key === 'NumpadSubtract') return 'numsub' 33 | if (key === 'NumpadMultiply') return 'nummult' 34 | if (key === 'NumpadDivide') return 'numdiv' 35 | if (key === 'NumpadDecimal') return 'numdec' 36 | if (key.startsWith('Numpad')) return `num${key.substring(6).toLowerCase()}` 37 | return key 38 | } 39 | 40 | export const shortcutAccelerator = (shortcut?: Shortcut | null): string => { 41 | // null check 42 | if (!shortcut || shortcut.key === disabledShortcutKey) { 43 | return null 44 | } 45 | 46 | // build accelerator 47 | let accelerator = '' 48 | if (shortcut.alt) accelerator += 'Alt+' 49 | if (shortcut.ctrl) accelerator += 'Control+' 50 | if (shortcut.shift) accelerator += 'Shift+' 51 | if (shortcut.meta) accelerator += 'Command+' 52 | 53 | // key 54 | accelerator += keyToAccelerator(shortcut.key) 55 | 56 | // done 57 | return accelerator 58 | } 59 | 60 | const registerShortcut = (name: string, shortcut: Shortcut, callback: () => void): void => { 61 | // check 62 | if (!shortcut || !callback) { 63 | return 64 | } 65 | 66 | // build accelerator 67 | const accelerator = shortcutAccelerator(shortcut) 68 | if (accelerator === null) { 69 | return 70 | } 71 | 72 | // debug 73 | console.debug('Registering shortcut', shortcut, accelerator) 74 | 75 | // do it 76 | try { 77 | globalShortcut.register(accelerator, callback) 78 | } catch (error) { 79 | console.error(`Failed to register shortcut for ${name}:`, error) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/mcp/init.ts: -------------------------------------------------------------------------------- 1 | import { McpClientObject, McpMetadataConfig, McpServerConfig, McpProgressCallback } from './types' 2 | // import { Notification } from 'electron' 3 | import { initializeClient } from './client' 4 | import { loadConfigFile } from './config' 5 | import Constants from '../utils/Constants' 6 | import { getMcpConfigForDxt } from './dxt' 7 | import path from 'node:path' 8 | 9 | export async function loadConfig(): Promise { 10 | try { 11 | const config = loadConfigFile(Constants.ASSETS_PATH.mcp) 12 | if (config) { 13 | console.log('Config loaded:', config) 14 | return Object.entries(config).map(([name, configJson]) => ({ name, configJson })) 15 | } 16 | } catch { 17 | return [] 18 | } 19 | } 20 | 21 | export async function initClients( 22 | metadata: McpMetadataConfig, 23 | callback?: McpProgressCallback 24 | ): Promise { 25 | if (!metadata) { 26 | console.log('NO clients initialized.') 27 | return [] 28 | } 29 | 30 | console.log('Config init:', metadata) 31 | 32 | try { 33 | const entries = Object.entries(metadata) 34 | const clientPromises = entries.map(async ([name, object]) => { 35 | if (object.type === 'metadata__stdio_config') { 36 | return initSingleClient(name, object.config, callback) 37 | } else if (object.type === 'metadata__mcpb_manifest') { 38 | const stdioConfig = await getMcpConfigForDxt( 39 | Constants.getPosixPath(path.join(Constants.ASSETS_PATH.mcpb, name)), 40 | object.config, 41 | object.user_config 42 | ) 43 | console.log(stdioConfig) 44 | return initSingleClient(name, stdioConfig, callback) 45 | } else { 46 | return { name } 47 | } 48 | }) 49 | 50 | const clients = await Promise.all(clientPromises) 51 | console.log('All clients initialized.') 52 | return clients 53 | } catch (error) { 54 | console.error('Error during client initialization:', error?.message) 55 | throw new Error(`${error?.message}`) 56 | } 57 | } 58 | 59 | export async function initSingleClient( 60 | name: string, 61 | serverConfig: McpServerConfig, 62 | callback?: McpProgressCallback 63 | ): Promise { 64 | console.log(`Initializing client for ${name} with config:`, serverConfig) 65 | const connection = await initializeClient(name, serverConfig, callback) 66 | console.log(`${name} initialized.`) 67 | const configJson = serverConfig 68 | return { name, connection, configJson } 69 | } 70 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | # AGENTS Guidelines for This Repository 2 | 3 | This repository contains an Electron application located in the root of this repository. When working on the project interactively with an agent (e.g. Codex, Claude Code) please follow the guidelines below so that the development experience – in particular Hot Module Replacement (HMR) – continues to work smoothly. 4 | 5 | ## 1. Use the Development Server, **not** `npm run build` 6 | 7 | - **Always use `npm run dev`** while iterating on the application. This starts Electron in development mode by Vite with hot-reload enabled. 8 | 9 | - **Do _not_ run `npm run build` inside the agent session.** If a production build is required, do it outside of the interactive agent workflow. 10 | 11 | ## 2. Keep Dependencies in Sync 12 | 13 | If you add or update dependencies remember to: 14 | 15 | 1. Update the appropriate lockfile (`package-lock.json`). 16 | 2. Re-start the development server so that Vite picks up the changes. 17 | 18 | ## 3. Coding Conventions 19 | 20 | - Prefer TypeScript (`.vue`/`.ts`) for new components and utilities. 21 | - Co-locate component-specific styles in the same folder as the component when practical. 22 | - For Vue-related components, prefer Vue 3 with the script setup syntax. 23 | - Use Pinia as the preferred state management. 24 | 25 | ## 4. Project Structure 26 | 27 | - When adding new files, be mindful of the differences between the renderer, main, and preload processes. 28 | - For components required by the renderer, most can be reused from Vuetify.js’s existing components — only create new ones when absolutely necessary. 29 | - For components needed by the main process, most can be reused from Electron’s built‑in APIs. 30 | - After adding new files, re-start the development server so that Vite picks up the changes. 31 | 32 | ## 5. Useful Commands Recap 33 | 34 | | Command | Purpose | 35 | | ------------------- | --------------------------------------------------------- | 36 | | `npm run dev` | Start the Vite dev server with HMR. | 37 | | `npm run lint:fix` | Run ESLint checks and fix. | 38 | | `npm run test` | Execute the test suite. | 39 | | `npm run build:pre` | **Production build – _do not run during agent sessions_** | 40 | 41 | --- 42 | 43 | Following these practices ensures that the agent-assisted development workflow stays fast and dependable. When in doubt, restart the dev server rather than running the production build. 44 | -------------------------------------------------------------------------------- /src/renderer/screens/PopupScreen.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 70 | 71 | 84 | -------------------------------------------------------------------------------- /vite.config.mts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url' 2 | import { defineConfig, loadEnv } from 'vite' 3 | import ElectronPlugin, { ElectronOptions } from 'vite-plugin-electron' 4 | import RendererPlugin from 'vite-plugin-electron-renderer' 5 | import VuetifyPlugin from 'vite-plugin-vuetify' 6 | import VueJsx from '@vitejs/plugin-vue-jsx' 7 | import Vue from '@vitejs/plugin-vue' 8 | import { rmSync } from 'fs' 9 | import { resolve, dirname } from 'path' 10 | import { builtinModules } from 'module' 11 | 12 | const isDevEnv = process.env.NODE_ENV === 'development' 13 | 14 | export default defineConfig(({ mode }) => { 15 | process.env = { 16 | ...(isDevEnv 17 | ? { 18 | ELECTRON_ENABLE_LOGGING: 'true' 19 | } 20 | : {}), 21 | ...process.env, 22 | ...loadEnv(mode, process.cwd()) 23 | } 24 | 25 | rmSync('dist', { recursive: true, force: true }) 26 | 27 | const electronPluginConfigs: ElectronOptions[] = [ 28 | { 29 | entry: 'src/main/index.ts', 30 | onstart({ startup }) { 31 | startup() 32 | }, 33 | vite: { 34 | root: resolve('.'), 35 | build: { 36 | assetsDir: '.', 37 | outDir: 'dist/main', 38 | rollupOptions: { 39 | external: ['electron', ...builtinModules] 40 | } 41 | } 42 | } 43 | }, 44 | { 45 | entry: 'src/preload/index.ts', 46 | onstart({ reload }) { 47 | reload() 48 | }, 49 | vite: { 50 | root: resolve('.'), 51 | build: { 52 | outDir: 'dist/preload' 53 | } 54 | } 55 | } 56 | ] 57 | 58 | return { 59 | define: { 60 | __VUE_I18N_FULL_INSTALL__: true, 61 | __VUE_I18N_LEGACY_API__: false, 62 | __INTLIFY_PROD_DEVTOOLS__: false 63 | }, 64 | resolve: { 65 | extensions: ['.mjs', '.js', '.ts', '.vue', '.json', '.scss'], 66 | alias: { 67 | '@': resolve(dirname(fileURLToPath(import.meta.url)), 'src') 68 | } 69 | }, 70 | base: './', 71 | root: resolve('./src/renderer'), 72 | publicDir: resolve('./src/renderer/public'), 73 | clearScreen: false, 74 | build: { 75 | sourcemap: isDevEnv, 76 | minify: 'terser', 77 | terserOptions: { 78 | compress: { 79 | drop_console: true, 80 | drop_debugger: true 81 | } 82 | }, 83 | outDir: resolve('./dist') 84 | }, 85 | plugins: [ 86 | Vue(), 87 | VueJsx(), 88 | // Docs: https://github.com/vuetifyjs/vuetify-loader 89 | VuetifyPlugin({ 90 | autoImport: true 91 | }), 92 | // Docs: https://github.com/electron-vite/vite-plugin-electron 93 | ElectronPlugin(electronPluginConfigs), 94 | RendererPlugin() 95 | ] 96 | } 97 | }) 98 | -------------------------------------------------------------------------------- /src/main/aid/automator.ts: -------------------------------------------------------------------------------- 1 | import { Application, Automator as AutomatorImpl } from './types' 2 | import { clipboard } from 'electron' 3 | import MacosAutomator from './macos' 4 | import WindowsAutomator from './windows' 5 | import NutAutomator from './nut' 6 | import Automation from './automation' 7 | 8 | export default class Automator { 9 | automator: AutomatorImpl 10 | 11 | constructor() { 12 | if (process.platform === 'darwin') { 13 | this.automator = new MacosAutomator() 14 | } else if (process.platform === 'win32') { 15 | this.automator = new WindowsAutomator() 16 | } else { 17 | this.automator = new NutAutomator() 18 | } 19 | } 20 | 21 | async getForemostApp(): Promise { 22 | try { 23 | return await this.automator.getForemostApp() 24 | } catch (error) { 25 | console.warn(error) 26 | return null 27 | } 28 | } 29 | 30 | async selectAll(): Promise { 31 | try { 32 | await this.automator.selectAll() 33 | } catch (error) { 34 | console.error(error) 35 | } 36 | } 37 | 38 | async moveCaretBelow(): Promise { 39 | try { 40 | await this.automator.moveCaretBelow() 41 | } catch (error) { 42 | console.error(error) 43 | } 44 | } 45 | 46 | async getSelectedText(): Promise { 47 | try { 48 | // save and set 49 | const clipboardText = clipboard.readText() 50 | clipboard.writeText('') 51 | 52 | // get it 53 | await this.automator.copySelectedText() 54 | const selectedText = clipboard.readText() 55 | 56 | // restore and done 57 | clipboard.writeText(clipboardText) 58 | return selectedText 59 | } catch (error) { 60 | console.error(error) 61 | return null 62 | } 63 | } 64 | 65 | async deleteSelectedText(): Promise { 66 | try { 67 | await this.automator.deleteSelectedText() 68 | } catch (error) { 69 | console.error(error) 70 | } 71 | } 72 | 73 | async pasteClipboard(): Promise { 74 | try { 75 | await this.automator.pasteText() 76 | } catch (error) { 77 | console.error(error) 78 | } 79 | } 80 | 81 | async pasteText(textToPaste: string): Promise { 82 | try { 83 | // save and set 84 | const clipboardText = clipboard.readText() 85 | 86 | // try to write text to clipboard 87 | const copied = await Automation.writeTextToClipboard(textToPaste) 88 | if (!copied) { 89 | return false 90 | } 91 | 92 | // paste it 93 | this.pasteClipboard() 94 | 95 | // restore 96 | await Automation.writeTextToClipboard(clipboardText) 97 | return true 98 | } catch (error) { 99 | console.error(error) 100 | return false 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/aid/windows.ts: -------------------------------------------------------------------------------- 1 | import { Application } from './types' 2 | import { Configuration } from './config' 3 | import { runVbs } from '@el3um4s/run-vbs' 4 | import NutAutomator from './nut' 5 | 6 | export default class extends NutAutomator { 7 | config: Configuration 8 | 9 | constructor() { 10 | super() 11 | this.setup() 12 | } 13 | 14 | async getForemostApp(): Promise { 15 | console.warn('getForemostApp not implemented (expected)') 16 | return null 17 | } 18 | 19 | async selectAll() { 20 | const script = ` 21 | Set WshShell = WScript.CreateObject("WScript.Shell") 22 | WshShell.SendKeys "^a" 23 | WScript.Sleep 200 24 | ` 25 | 26 | // run it 27 | await runVbs({ vbs: script }) 28 | } 29 | 30 | async moveCaretBelow() { 31 | const script = ` 32 | Set WshShell = WScript.CreateObject("WScript.Shell") 33 | WshShell.SendKeys "{DOWN}{ENTER}" 34 | WScript.Sleep 200 35 | ` 36 | 37 | // run it 38 | await runVbs({ vbs: script }) 39 | } 40 | 41 | async copySelectedText() { 42 | try { 43 | if (!(await this.setup())) throw new Error('nutjs not loaded') 44 | await this.nut().keyboard.pressKey(this.commandKey(), this.nut().Key.C) 45 | await this.nut().keyboard.releaseKey(this.commandKey(), this.nut().Key.C) 46 | } catch { 47 | // fallback to vbs 48 | const script = ` 49 | Set WshShell = WScript.CreateObject("WScript.Shell") 50 | WshShell.SendKeys "^c" 51 | WScript.Sleep 20 52 | ` 53 | 54 | // run it 55 | await runVbs({ vbs: script }) 56 | } 57 | } 58 | 59 | async pasteText() { 60 | try { 61 | // nut is faster but not always available 62 | if (!(await this.setup())) throw new Error('nutjs not loaded') 63 | await this.nut().keyboard.pressKey(this.commandKey(), this.nut().Key.V) 64 | await this.nut().keyboard.releaseKey(this.commandKey(), this.nut().Key.V) 65 | } catch { 66 | // fallback to vbs 67 | const script = ` 68 | Set WshShell = WScript.CreateObject("WScript.Shell") 69 | WshShell.SendKeys "^v" 70 | WScript.Sleep 20 71 | ` 72 | 73 | // run it 74 | await runVbs({ vbs: script }) 75 | } 76 | } 77 | 78 | async deleteSelectedText() { 79 | const script = ` 80 | Set WshShell = WScript.CreateObject("WScript.Shell") 81 | WshShell.SendKeys "{DELETE}" 82 | WScript.Sleep 200 83 | ` 84 | 85 | // run it 86 | await runVbs({ vbs: script }) 87 | } 88 | 89 | async activateApp(title: string) { 90 | const script = ` 91 | Set WshShell = WScript.CreateObject("WScript.Shell") 92 | WshShell.AppActivate "${title}" 93 | WScript.Sleep 200 94 | ` 95 | 96 | // run it 97 | await runVbs({ vbs: script }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/mcp/dxt.ts: -------------------------------------------------------------------------------- 1 | import { 2 | unpackExtension, 3 | getMcpConfigForManifest, 4 | v0_3 as McpbVersion, 5 | McpbManifestAny, 6 | McpbUserConfigValues, 7 | Logger 8 | } from '@anthropic-ai/mcpb' 9 | 10 | import type { McpDxtErrors } from '@/types/mcp' 11 | 12 | import { existsSync, readFileSync, statSync } from 'fs' 13 | import { join, resolve, sep } from 'path' 14 | 15 | export type McpServerConfig = McpbManifestAny['server']['mcp_config'] 16 | 17 | const mockSystemDirs = { 18 | home: '/home/user', 19 | data: '/data' 20 | } 21 | 22 | export async function getMcpConfigForDxt( 23 | basePath: string, 24 | baseManifest: McpbManifestAny, 25 | userConfig: McpbUserConfigValues 26 | ): Promise { 27 | const logMessages: string[] = [] 28 | const logger: Logger = { 29 | log: (...args: unknown[]) => logMessages.push(args.join(' ')), 30 | warn: (...args: unknown[]) => logMessages.push(args.join(' ')), 31 | error: (...args: unknown[]) => logMessages.push(args.join(' ')) 32 | } 33 | 34 | const mcpConfig = await getMcpConfigForManifest({ 35 | manifest: baseManifest, 36 | extensionPath: basePath, 37 | systemDirs: mockSystemDirs, 38 | userConfig: userConfig, 39 | pathSeparator: sep, 40 | logger 41 | }) 42 | 43 | if (mcpConfig === undefined) { 44 | throw new Error(logMessages.join('\n')) 45 | } else { 46 | return mcpConfig 47 | } 48 | } 49 | export async function unpackDxt(dxtUnpackOption: { 50 | mcpbPath: string 51 | outputDir: string 52 | }): Promise { 53 | return unpackExtension(dxtUnpackOption) 54 | } 55 | 56 | export function getManifest(inputPath: string): McpDxtErrors | McpbManifestAny { 57 | try { 58 | const resolvedPath = resolve(inputPath) 59 | let manifestPath = resolvedPath 60 | 61 | // If input is a directory, look for manifest.json inside it 62 | if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) { 63 | manifestPath = join(resolvedPath, 'manifest.json') 64 | } 65 | 66 | const manifestContent = readFileSync(manifestPath, 'utf-8') 67 | const manifestData = JSON.parse(manifestContent) 68 | 69 | const result = McpbVersion.McpbManifestSchema.safeParse(manifestData) 70 | 71 | if (result.success) { 72 | console.log('Manifest is valid!') 73 | return result.data 74 | } else { 75 | console.log('ERROR: Manifest validation failed:\n') 76 | const errors = result.error.issues.map((issue) => { 77 | const path = issue.path.join('.') 78 | console.log(` - ${path ? `${path}: ` : ''}${issue.message}`) 79 | return { 80 | field: path, 81 | message: issue.message 82 | } 83 | }) 84 | return { errors: errors } 85 | } 86 | } catch (error) { 87 | const dxtError = { 88 | field: 'manifest', 89 | message: error instanceof Error ? error.message : String(error) 90 | } 91 | 92 | console.error(dxtError.message) 93 | return { errors: [dxtError] } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/renderer/store/history.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import localForage from 'localforage' 3 | import { v4 as uuidv4 } from 'uuid' 4 | import { useMessageStore } from '@/renderer/store/message' 5 | 6 | import type { ChatConversationMessage } from '@/renderer/types/message' 7 | import type { SessionId, SessionEntry } from '@/renderer/types/session' 8 | 9 | interface HistoryStoreState { 10 | // selected: SessionId[] | undefined 11 | conversations: SessionEntry[] 12 | } 13 | 14 | export const useHistoryStore = defineStore('historyStore', { 15 | state: (): HistoryStoreState => ({ 16 | // selected: undefined as SessionId[] | undefined, 17 | conversations: [] as SessionEntry[] 18 | }), 19 | persist: { 20 | include: ['conversations'], 21 | storage: localForage 22 | }, 23 | getters: {}, 24 | actions: { 25 | getDate() { 26 | const date = new Date().toLocaleString('zh', { timeZoneName: 'short', hour12: false }) 27 | return `${date} ${uuidv4()}` 28 | }, 29 | resetState() { 30 | this.$reset() 31 | }, 32 | deleteById(index: number) { 33 | this.conversations.splice(index, 1) 34 | }, 35 | init(conversation: SessionEntry) { 36 | const oldConversation = this.find(conversation.id) 37 | console.log(conversation) 38 | if (!oldConversation) { 39 | const newId = this.getDate() 40 | this.conversations.unshift({ 41 | ...conversation, 42 | id: newId 43 | }) 44 | // this.selected = [newId] 45 | return this.conversations[0] 46 | } else { 47 | return oldConversation 48 | } 49 | }, 50 | find(id: string | undefined) { 51 | if (id) { 52 | return this.conversations.find((item) => item.id === id) 53 | } else { 54 | return undefined 55 | } 56 | }, 57 | select(sessionId: SessionId) { 58 | const conversation = this.find(sessionId) 59 | if (conversation) { 60 | const messageStore = useMessageStore() 61 | messageStore.setConversation(conversation) 62 | } 63 | }, 64 | getColor(index: number) { 65 | const targetElement = this.conversations[index]?.messages.find( 66 | (element) => element.role === 'assistant' 67 | ) 68 | if (targetElement) { 69 | return 'primary' 70 | } else { 71 | return 'grey' 72 | } 73 | }, 74 | downloadById(index: number) { 75 | const name = this.conversations[index].id.replace(/[/: ]/g, '-') 76 | this.download(this.conversations[index].messages, `history-${name}.json`) 77 | }, 78 | downloadHistory() { 79 | this.download(this.conversations, 'history.json') 80 | }, 81 | download(json: ChatConversationMessage[] | SessionEntry[], filename: string) { 82 | const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' }) 83 | const url = URL.createObjectURL(blob) 84 | const a = document.createElement('a') 85 | a.href = url 86 | a.download = filename 87 | a.click() 88 | URL.revokeObjectURL(url) 89 | } 90 | } 91 | }) 92 | -------------------------------------------------------------------------------- /docs/src/zhHans/installation-and-build/build-configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 2 3 | --- 4 | 5 | # 构建配置 6 | 7 | 模块安装完成后,只需执行以下命令即可构建平台软件包。 8 | 9 | ```shell 10 | # For Windows (.exe, .appx) 11 | $ npm run build:win 12 | 13 | # For macOS (.dmg) 14 | $ npm run build:mac 15 | 16 | # For Linux (.rpm, .deb, .snap) 17 | $ npm run build:linux 18 | 19 | # All platform (.exe, .appx, .dmg, .rpm, .deb, .snap) - see below description 20 | $ npm run build:all 21 | ``` 22 | 23 | 已构建的软件包可在 `release/{version}` 位置找到。 24 | 25 | 如需了解更多信息,请参阅以下文章: https://webpack.electron.build/dependency-management#installing-native-node-modules 26 | 27 | ## 多平台构建需要做些什么? 28 | 29 | 要为每个操作系统创建软件包,必须在相同的操作系统上构建。例如,macOS 的软件包必须在 macOS 机器上构建。 30 | 31 | 不过,你可以在一个操作系统上同时为 Windows、macOS 和 Linux 构建软件包。不过,这可能需要一些准备工作。 32 | 33 | 如果想在一个平台上同时构建多个平台,建议使用**macOS**。因为只需几个非常简单的设置就能对其进行配置。 34 | 35 | 您可以使用以下命令同时执行多平台构建。或者,你也可以通过上面的单独构建命令,只针对你想要的操作系统进行构建。 36 | 37 | ```shell 38 | $ npm run build:all 39 | ``` 40 | 41 | Linux 构建可能需要 "Multipass" 配置。通过以下链接了解有关 `Multipass` 的更多信息: https://multipass.run 42 | 43 | 要了解有关多平台构建的更多信息,请参阅以下文章: https://electron.build/multi-platform-build 44 | 45 | ## 通过排除开发文件减少软件包大小 46 | 47 | 您可以通过在 `buildAssets/builder/config.ts` 的 files 属性中添加文件模式,在构建时排除不需要的文件。这将节省捆绑包的容量。 48 | 49 | 下面是一个不必要的 `node_modules` 文件模式,可以进一步节省捆绑包。根据项目情况,使用下面的规则可能会导致问题,因此请在使用前进行审查。 50 | 51 | ```json 52 | [ 53 | "!**/.*", 54 | "!**/node_modules/**/{CONTRIBUTORS,CNAME,AUTHOR,TODO,CONTRIBUTING,COPYING,INSTALL,NEWS,PORTING,Makefile,htdocs,CHANGELOG,ChangeLog,changelog,README,Readme,readme,test,sample,example,demo,composer.json,tsconfig.json,jsdoc.json,tslint.json,typings.json,gulpfile,bower.json,package-lock,Gruntfile,CMakeLists,karma.conf,yarn.lock}*", 55 | "!**/node_modules/**/{man,benchmark,node_modules,spec,cmake,browser,vagrant,doxy*,bin,obj,obj.target,example,examples,test,tests,doc,docs,msvc,Xcode,CVS,RCS,SCCS}{,/**/*}", 56 | "!**/node_modules/**/*.{conf,png,pc,coffee,txt,spec.js,ts,js.flow,html,def,jst,xml,ico,in,ac,sln,dsp,dsw,cmd,vcproj,vcxproj,vcxproj.filters,pdb,exp,obj,lib,map,md,sh,gypi,gyp,h,cpp,yml,log,tlog,Makefile,mk,c,cc,rc,xcodeproj,xcconfig,d.ts,yaml,hpp}", 57 | "!**/node_modules/**/node-v*-x64{,/**/*}", 58 | "!**/node_modules/bluebird/js/browser{,/**/*}", 59 | "!**/node_modules/bluebird/js/browser{,/**/*}", 60 | "!**/node_modules/source-map/dist{,/**/*}", 61 | "!**/node_modules/lodash/fp{,/**/*}", 62 | "!**/node_modules/async/!(dist|package.json)", 63 | "!**/node_modules/async/internal{,/**/*}", 64 | "!**/node_modules/ajv/dist{,/**/*}", 65 | "!**/node_modules/ajv/scripts{,/**/*}", 66 | "!**/node_modules/node-pre-gyp/!(lib|package.json)", 67 | "!**/node_modules/node-pre-gyp/lib/!(util|pre-binding.js|node-pre-gyp.js)", 68 | "!**/node_modules/node-pre-gyp/lib/util/!(versioning.js|abi_crosswalk.json)", 69 | "!**/node_modules/source-map-support/browser-source-map-support.js", 70 | "!**/node_modules/json-schema/!(package.json|lib)" 71 | ] 72 | ``` 73 | 74 | ## 使用本地 Node 模块的项目的构建设置 75 | 76 | 对于使用 **Native Node Module**的项目,请将以下脚本添加到您的 `package.json`: 安装依赖项时,`electron-builder` 会处理任何需要重建的模块。 77 | 78 | ```json 79 | { 80 | "scripts": { 81 | "postinstall": "electron-builder install-app-deps" 82 | } 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /src/main/mcp/config.ts: -------------------------------------------------------------------------------- 1 | import { showNotification } from '../utils//notification' 2 | import fs from 'node:fs' 3 | import path from 'node:path' 4 | import { McpServerConfig } from './types' 5 | import { shell } from 'electron' 6 | import { debounce } from 'lodash' 7 | 8 | let mcpConfig 9 | 10 | let timeoutId 11 | 12 | const DEBOUNCE_DELAY = 500 13 | 14 | function handleFileChange(configPath) { 15 | try { 16 | const parsedConfig = readConfig(configPath) 17 | if (mcpConfig !== parsedConfig) { 18 | console.log(`${configPath} changed`) 19 | } 20 | } catch {} 21 | } 22 | 23 | const debouncedHandleFileChange = debounce(handleFileChange, DEBOUNCE_DELAY) 24 | 25 | function readConfig(configPath: string) { 26 | const fileString = fs.readFileSync(configPath, 'utf8') 27 | if (fileString) { 28 | return JSON.parse(fileString) 29 | } else { 30 | return {} 31 | } 32 | } 33 | 34 | export function loadConfigFile(configPath: string): Record { 35 | const resolvedConfigPath = path.isAbsolute(configPath) 36 | ? configPath 37 | : path.resolve(process.cwd(), configPath) 38 | 39 | try { 40 | if (!fs.existsSync(resolvedConfigPath)) { 41 | showNotification({ 42 | body: `MCP Config Not Found.` 43 | }) 44 | return {} 45 | } 46 | const parsedConfig = readConfig(resolvedConfigPath) 47 | mcpConfig = parsedConfig 48 | fs.watch(resolvedConfigPath, () => { 49 | if (timeoutId) clearTimeout(timeoutId) 50 | timeoutId = setTimeout(() => debouncedHandleFileChange(resolvedConfigPath), DEBOUNCE_DELAY) 51 | }) 52 | if (!parsedConfig.mcpServers) { 53 | return {} 54 | } else { 55 | return parsedConfig.mcpServers 56 | } 57 | } catch (err) { 58 | showNotification( 59 | { 60 | body: `MCP Config JSON parse failure` 61 | }, 62 | 63 | { 64 | onClick: () => { 65 | shell.showItemInFolder(resolvedConfigPath) 66 | } 67 | } 68 | ) 69 | if (err instanceof SyntaxError) { 70 | throw new Error(`Invalid JSON in config file: ${err.message}`) 71 | } 72 | throw err 73 | } 74 | } 75 | 76 | export function loadLlmFile(llmPath: string) { 77 | const resolvedConfigPath = path.isAbsolute(llmPath) 78 | ? llmPath 79 | : path.resolve(process.cwd(), llmPath) 80 | try { 81 | if (!fs.existsSync(resolvedConfigPath)) { 82 | throw new Error(`Config file not found: ${resolvedConfigPath}`) 83 | } 84 | const configContent = fs.readFileSync(resolvedConfigPath, 'utf8') 85 | const parsedConfig = JSON.parse(configContent) 86 | if (!parsedConfig) { 87 | return {} 88 | } else { 89 | return parsedConfig 90 | } 91 | } catch (err) { 92 | showNotification( 93 | { 94 | body: 'LLM Config JSON parse failure' 95 | }, 96 | 97 | { 98 | onClick: () => { 99 | shell.showItemInFolder(resolvedConfigPath) 100 | } 101 | } 102 | ) 103 | if (err instanceof SyntaxError) { 104 | throw new Error(`Invalid JSON in llm file: ${err.message}`) 105 | } 106 | throw err 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/renderer/components/pages/McpProcessPage.vue: -------------------------------------------------------------------------------- 1 | 67 | 68 | 102 | -------------------------------------------------------------------------------- /src/renderer/components/pages/McpEditPage.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 115 | -------------------------------------------------------------------------------- /docs/src/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { withSidebar } from 'vitepress-sidebar' 2 | import pkg from '../../../package.json' with { type: 'json' } 3 | import { defineConfig, UserConfig } from 'vitepress' 4 | import { withI18n } from 'vitepress-i18n' 5 | import type { VitePressSidebarOptions } from 'vitepress-sidebar/types' 6 | import type { VitePressI18nOptions } from 'vitepress-i18n/types' 7 | 8 | const { name, repository, homepage } = pkg 9 | const capitalizeFirst = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1) 10 | const supportLocales = ['en', 'zhHans'] 11 | const defaultLocale: string = supportLocales[0] 12 | 13 | const vitePressI18nConfigs: VitePressI18nOptions = { 14 | locales: supportLocales, 15 | rootLocale: defaultLocale, 16 | searchProvider: 'local', 17 | description: { 18 | en: 'TUUI is a free and open-source desktop application designed for AI agentic systems, leveraging the Model Context Protocol.', 19 | zhHans: 20 | 'TUUI 是一款基于模型上下文协议(Model Context Protocol)、专为AI智能体系统设计的免费开源桌面应用程序。' 21 | }, 22 | themeConfig: { 23 | en: { 24 | nav: [ 25 | { 26 | text: 'Getting Started', 27 | link: '/installation-and-build/getting-started' 28 | } 29 | ] 30 | }, 31 | zhHans: { 32 | nav: [ 33 | { 34 | text: '入门', 35 | link: '/zhHans/installation-and-build/getting-started' 36 | } 37 | ] 38 | } 39 | } 40 | } 41 | 42 | const vitePressSidebarConfigs: VitePressSidebarOptions[] = [ 43 | ...supportLocales.map((lang) => { 44 | return { 45 | collapsed: false, 46 | useTitleFromFileHeading: true, 47 | useTitleFromFrontmatter: true, 48 | useFolderTitleFromIndexFile: true, 49 | sortMenusByFrontmatterOrder: true, 50 | hyphenToSpace: true, 51 | capitalizeEachWords: true, 52 | manualSortFileNameByPriority: [ 53 | 'introduction.md', 54 | 'installation-and-build', 55 | 'project-structures', 56 | 'electron-how-to' 57 | ], 58 | documentRootPath: `/src/${lang}`, 59 | resolvePath: defaultLocale === lang ? '/' : `/${lang}/`, 60 | ...(defaultLocale === lang ? {} : { basePath: `/${lang}/` }) 61 | } 62 | }) 63 | ] 64 | 65 | const vitePressConfigs: UserConfig = { 66 | title: capitalizeFirst(name), 67 | lastUpdated: true, 68 | outDir: '../dist', 69 | head: [ 70 | ['link', { rel: 'icon', href: '/logo.png' }], 71 | ['link', { rel: 'shortcut icon', href: '/favicon.ico' }], 72 | [ 73 | 'meta', 74 | { 75 | name: 'keywords', 76 | content: 77 | 'MCP, Model Context Protocol, Client, TUUI, AI, agent, LLM, 模型上下文协议, 桌面应用, 开源工具, AI代理, 开发者工具, AI开发, 工具调用' 78 | } 79 | ] 80 | ], 81 | cleanUrls: true, 82 | metaChunk: true, 83 | rewrites: { 84 | 'en/:rest*': ':rest*' 85 | }, 86 | sitemap: { 87 | hostname: homepage 88 | }, 89 | themeConfig: { 90 | logo: { src: '/icon.png', width: 24, height: 24 }, 91 | editLink: { 92 | pattern: 'https://github.com/AI-QL/tuui/edit/main/docs/src/:path' 93 | }, 94 | socialLinks: [{ icon: 'github', link: repository.url.replace('.git', '') }] 95 | } 96 | } 97 | 98 | export default defineConfig( 99 | withSidebar(withI18n(vitePressConfigs, vitePressI18nConfigs), vitePressSidebarConfigs) 100 | ) 101 | -------------------------------------------------------------------------------- /src/renderer/components/pages/McpNewsPage.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 81 | 82 | 87 | -------------------------------------------------------------------------------- /src/types/mcp.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CallToolRequest, 3 | CallToolResult, 4 | ListToolsRequest, 5 | ListToolsResult, 6 | ListResourcesRequest, 7 | ListResourcesResult, 8 | ReadResourceRequest, 9 | ReadResourceResult, 10 | ListPromptsRequest, 11 | ListPromptsResult, 12 | GetPromptRequest, 13 | GetPromptResult, 14 | Implementation as McpServerImplementation 15 | } from '@modelcontextprotocol/sdk/types.d.ts' 16 | 17 | import { StdioServerParameters } from '@modelcontextprotocol/sdk/client/stdio.js' 18 | 19 | import { McpbManifestAny, McpbUserConfigValues } from '@anthropic-ai/mcpb' 20 | 21 | export type TypedAsyncFunction = (..._args: Args) => Promise 22 | 23 | export type McpAsyncToolsList = TypedAsyncFunction<[ListToolsRequest], ListToolsResult> 24 | export type McpAsyncToolsCall = TypedAsyncFunction<[CallToolRequest], CallToolResult> 25 | 26 | export type McpAsyncPromptsList = TypedAsyncFunction<[ListPromptsRequest], ListPromptsResult> 27 | export type McpAsyncPromptsGet = TypedAsyncFunction<[GetPromptRequest], GetPromptResult> 28 | 29 | export type McpAsyncResourcesList = TypedAsyncFunction<[ListResourcesRequest], ListResourcesResult> 30 | export type McpAsyncResourcesRead = TypedAsyncFunction<[ReadResourceRequest], ReadResourceResult> 31 | 32 | export type AsyncFunction = 33 | | McpAsyncToolsList 34 | | McpAsyncToolsCall 35 | | McpAsyncPromptsList 36 | | McpAsyncPromptsGet 37 | | McpAsyncResourcesList 38 | | McpAsyncResourcesRead 39 | 40 | export type userConfigValue = McpbUserConfigValues[string] 41 | 42 | export type McpMetadataStdio = { 43 | name: string 44 | type: 'metadata__stdio_config' 45 | config: StdioServerParameters 46 | description?: McpServerDescription 47 | } 48 | 49 | type McpDxtError = { 50 | field: string 51 | message: string 52 | } 53 | 54 | export type McpDxtErrors = { 55 | errors: McpDxtError[] 56 | } 57 | 58 | export type { McpbManifestAny } 59 | 60 | export type McpMetadataDxt = { 61 | name: string 62 | type: 'metadata__mcpb_manifest' 63 | config: McpbManifestAny | McpDxtErrors 64 | user_config?: McpbUserConfigValues 65 | } 66 | 67 | export type McpMetadata = McpMetadataStdio | McpMetadataDxt 68 | 69 | export type McpObject = { 70 | metadata?: McpMetadata 71 | tools?: { 72 | list?: McpAsyncToolsList 73 | call?: McpAsyncToolsCall 74 | } 75 | prompts?: { 76 | list?: McpAsyncPromptsList 77 | get?: McpAsyncPromptsGet 78 | } 79 | resources?: { 80 | list?: McpAsyncResourcesList 81 | read?: McpAsyncResourcesRead 82 | } 83 | } 84 | 85 | export type McpToolType = ListToolsResult['tools'][number] 86 | 87 | export type ToolType = { 88 | name: McpToolType['name'] 89 | description: McpToolType['description'] 90 | // Rename inputSchema to parameters to comply with the OpenAI SDK 91 | parameters: McpToolType['inputSchema'] 92 | } 93 | 94 | export type MCPAPI = Record 95 | 96 | export type DXTAPI = Record 97 | 98 | export type ClientProfile = { 99 | name: string 100 | tools?: Record 101 | prompts?: Record 102 | resources?: Record 103 | config?: StdioServerParameters 104 | description?: McpServerDescription 105 | } 106 | 107 | export type McpServerDescription = { 108 | instructions: string 109 | implementation: McpServerImplementation 110 | } 111 | -------------------------------------------------------------------------------- /src/main/aid/automation.ts: -------------------------------------------------------------------------------- 1 | // import { Application } from './types' 2 | import { clipboard } from 'electron' 3 | import Automator from './automator' 4 | // import * as window from '../main/window' 5 | import { wait } from './utils' 6 | 7 | const kWriteTextTimeout = 1000 8 | 9 | // export enum AutomationAction { 10 | // INSERT_BELOW, 11 | // REPLACE 12 | // } 13 | 14 | export default class Automation { 15 | static grabSelectedText = async ( 16 | automator: Automator, 17 | timeout: number = 1000 18 | ): Promise => { 19 | // wait for focus 20 | await wait(10) 21 | 22 | // get started 23 | const start = new Date().getTime() 24 | 25 | // grab text repeatedly 26 | let text = null 27 | const grabStart = new Date().getTime() 28 | while (true) { 29 | text = await automator.getSelectedText() 30 | if (text != null && text.trim() !== '') { 31 | break 32 | } 33 | if (new Date().getTime() - grabStart > timeout) { 34 | console.log(`Grab text timeout after ${timeout}ms`) 35 | break 36 | } 37 | await wait(100) 38 | } 39 | 40 | // log 41 | if (text?.length) { 42 | console.debug( 43 | `Text grabbed: ${text.trimEnd().slice(0, 50)}… [${new Date().getTime() - start}ms]` 44 | ) 45 | } 46 | 47 | // done 48 | return text 49 | } 50 | 51 | // static automate = async (text: string, sourceApp: Application|null, action: AutomationAction): Promise => { 52 | 53 | // try { 54 | 55 | // const result = text; 56 | 57 | // // copy to clipboard 58 | // const clipboardText = clipboard.readText(); 59 | // const copied = await Automation.writeTextToClipboard(result); 60 | // if (!copied) { 61 | // return false; 62 | // } 63 | 64 | // // close prompt anywhere 65 | // await window.closePromptAnywhere(sourceApp); 66 | 67 | // // now paste 68 | // console.debug(`Processing LLM output: "${result.slice(0, 50)}"…`); 69 | 70 | // // we need an automator 71 | // const automator = new Automator(); 72 | // if (action === AutomationAction.INSERT_BELOW) { 73 | // await automator.moveCaretBelow() 74 | // await automator.pasteClipboard() 75 | // } else if (action === AutomationAction.REPLACE) { 76 | // await automator.deleteSelectedText() 77 | // await automator.pasteClipboard() 78 | // } 79 | 80 | // // restore clipboard 81 | // await Automation.writeTextToClipboard(clipboardText); 82 | 83 | // // done 84 | // return true; 85 | 86 | // } catch (error) { 87 | // console.error('Error while testing', error); 88 | // } 89 | 90 | // // too bad 91 | // return false; 92 | 93 | // } 94 | 95 | static writeTextToClipboard = async (text: string): Promise => { 96 | // we try several times in case something goes wrong (rare but...) 97 | const start = new Date().getTime() 98 | while (true) { 99 | try { 100 | clipboard.writeText(text) 101 | if (clipboard.readText() === text) { 102 | return true 103 | } 104 | } catch { 105 | /* empty */ 106 | } 107 | 108 | const now = new Date().getTime() 109 | if (now - start > kWriteTextTimeout) { 110 | return false 111 | } 112 | 113 | // wait 114 | await wait(100) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/assets/config/llm.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "name": "", 4 | "apiKey": "", 5 | "apiCli": "", 6 | "icon": "", 7 | "url": "https://api2.aiql.com", 8 | "urlList": ["https://api2.aiql.com"], 9 | "path": "/chat/completions", 10 | "pathList": [ 11 | "/chat/completions", 12 | "/v1/chat/completions", 13 | "/v1/openai/chat/completions", 14 | "/openai/v1/chat/completions" 15 | ], 16 | "model": "Qwen/Qwen3-32B", 17 | "modelList": [ 18 | "Qwen/Qwen3-32B", 19 | "Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo", 20 | "openai/gpt-oss-120b" 21 | ], 22 | "authPrefix": "Bearer", 23 | "authPrefixList": ["Bearer", "Base", "Token"], 24 | "maxTokensPrefixList": ["max_tokens", "max_completion_tokens", "max_new_tokens"], 25 | "maxTokensPrefix": "max_tokens", 26 | "maxTokensValue": null, 27 | "temperature": null, 28 | "topP": null, 29 | "method": "POST", 30 | "contentType": "application/json", 31 | "stream": true, 32 | "reasoningEffort": null, 33 | "enableThinking": null, 34 | "authorization": true, 35 | "mcp": true 36 | }, 37 | "custom": [ 38 | { 39 | "name": "Chatbot Default", 40 | "icon": "https://www.tuui.com/favicon.ico" 41 | }, 42 | { 43 | "url": "https://dashscope.aliyuncs.com/compatible-mode", 44 | "urlList": ["https://dashscope.aliyuncs.com/compatible-mode"], 45 | "path": "/v1/chat/completions", 46 | "name": "Qwen", 47 | "icon": "https://g.alicdn.com/qwenweb/qwen-ai-fe/0.0.19/favicon.ico", 48 | "model": "qwen-turbo", 49 | "modelList": ["qwen-turbo", "qwen-plus", "qwen-max"] 50 | }, 51 | { 52 | "url": "https://api.deepseek.com", 53 | "urlList": ["https://api.deepseek.com"], 54 | "name": "DeepSeek", 55 | "icon": "https://www.deepseek.com/favicon.ico", 56 | "model": "deepseek-chat", 57 | "modelList": ["deepseek-chat"] 58 | }, 59 | { 60 | "url": "https://api.openai.com", 61 | "urlList": ["https://api.openai.com", "https://api.aiql.com"], 62 | "name": "OpenAI & Proxy", 63 | "icon": "https://openai.com/favicon.ico", 64 | "model": "gpt-5", 65 | "modelList": ["gpt-5", "gpt-4.1", "o1", "o3"], 66 | "maxTokensPrefix": "max_completion_tokens" 67 | }, 68 | { 69 | "url": "https://api.deepinfra.com", 70 | "urlList": ["https://api.deepinfra.com"], 71 | "path": "/v1/openai/chat/completions", 72 | "name": "DeepInfra", 73 | "icon": "https://deepinfra.com/favicon.ico", 74 | "model": "Qwen/Qwen3-32B", 75 | "modelList": ["Qwen/Qwen3-32B", "Qwen/Qwen3-30B-A3B", "meta-llama/Llama-3.3-70B-Instruct"] 76 | }, 77 | { 78 | "name": "Open Router & Proxy", 79 | "icon": "https://openrouter.ai/favicon.ico", 80 | "url": "https://openrouter.ai/api", 81 | "urlList": ["https://openrouter.ai/api", "https://api3.aiql.com"], 82 | "path": "/v1/chat/completions", 83 | "model": "openai/gpt-5-chat", 84 | "modelList": [ 85 | "openai/gpt-5-chat", 86 | "openai/gpt-oss-120b", 87 | "openai/gpt-oss-20b", 88 | "qwen/qwen3-coder:free" 89 | ] 90 | }, 91 | { 92 | "name": "Chat Anywhere", 93 | "icon": "https://api.chatanywhere.org/favicon.ico", 94 | "url": "https://api.chatanywhere.tech", 95 | "urlList": ["https://api.chatanywhere.tech", "https://api.chatanywhere.org"], 96 | "path": "/chat/completions", 97 | "model": "gpt-5", 98 | "modelList": ["gpt-5", "gpt-4.1", "o3", "o1", "claude-opus-4-20250514"] 99 | } 100 | ] 101 | } 102 | -------------------------------------------------------------------------------- /src/main/mcp/client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateMessageRequestSchema as SamplingRequestSchema, 3 | ElicitRequestSchema 4 | } from '@modelcontextprotocol/sdk/types.js' 5 | import { Client } from '@modelcontextprotocol/sdk/client/index.js' 6 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' 7 | 8 | import { McpServerConfig, McpClientTransport, McpProgressCallback } from './types' 9 | import { connect } from './connection' 10 | 11 | import { samplingTransferInvoke, elicitationTransferInvoke } from '../index' 12 | import Constants from '../utils/Constants' 13 | 14 | export async function initializeClient( 15 | name: string, 16 | serverConfig: McpServerConfig, 17 | callback?: McpProgressCallback, 18 | idleTimeout: number = 90 // 90 sec by default 19 | ): Promise { 20 | let idleTimer: NodeJS.Timeout 21 | let rejectFn: (_reason: Error) => void 22 | 23 | const resetTimer = () => { 24 | clearTimeout(idleTimer) 25 | idleTimer = setTimeout(() => { 26 | rejectFn( 27 | new Error( 28 | `Initialization of client for ${name} timed out after ${idleTimeout} seconds of inactivity` 29 | ) 30 | ) 31 | }, idleTimeout * 1000) 32 | } 33 | 34 | const timeoutPromise = new Promise((_, reject) => { 35 | rejectFn = reject 36 | resetTimer() // Init timer 37 | }) 38 | 39 | const stdioPromise = initializeStdioClient(name, serverConfig, callback, resetTimer) 40 | 41 | return Promise.race([stdioPromise, timeoutPromise]) 42 | } 43 | 44 | async function initializeStdioClient( 45 | name: string, 46 | config: McpServerConfig, 47 | callback?: McpProgressCallback, 48 | onData?: () => void 49 | ): Promise { 50 | const transport = new StdioClientTransport({ 51 | ...config, 52 | stderr: 'pipe' 53 | }) 54 | const clientName = `${name}-client` 55 | const client = new Client( 56 | { 57 | name: clientName, 58 | version: Constants.APP_VERSION 59 | }, 60 | { 61 | capabilities: { 62 | sampling: {}, 63 | elicitation: {} 64 | } 65 | } 66 | ) 67 | 68 | if (transport.stderr) { 69 | transport.stderr.on('data', (chunk) => { 70 | if (onData) onData() 71 | if (callback) callback(name, chunk.toString(), 'pending') 72 | }) 73 | } 74 | 75 | try { 76 | callback(name, 'Staring...', 'pending') 77 | await connect(client, transport) 78 | console.log(`${clientName} connected.`) 79 | callback(name, 'Done', 'success') 80 | } catch (error) { 81 | const errorMsg = error instanceof Error ? error.message : String(error) 82 | callback(name, errorMsg, 'error') 83 | throw error 84 | } 85 | 86 | client.setRequestHandler(SamplingRequestSchema, async (request) => { 87 | console.log('Sampling request received:\n', request) 88 | const response = await samplingTransferInvoke(request) 89 | console.log(response) 90 | return response 91 | }) 92 | 93 | client.setRequestHandler(ElicitRequestSchema, async (request) => { 94 | const response = await elicitationTransferInvoke(request) 95 | 96 | console.log('Elicitation request received:\n', JSON.stringify(response, null, 2)) 97 | 98 | return response 99 | }) 100 | 101 | return { client, transport } 102 | } 103 | 104 | export async function manageRequests(client: Client, method: string, schema: any, params?: any) { 105 | const requestObject = { 106 | method, 107 | ...(params && { params }) 108 | } 109 | 110 | const result = await client.request(requestObject, schema) 111 | console.log(result) 112 | return result 113 | } 114 | -------------------------------------------------------------------------------- /src/main/utils/Constants.ts: -------------------------------------------------------------------------------- 1 | import { join, dirname, normalize, sep } from 'path' 2 | import { name, version, debug, homepage, schemaVersion } from '../../../package.json' 3 | import { fileURLToPath } from 'url' 4 | import { app } from 'electron' 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)) 7 | 8 | export interface TrayOptions { 9 | enabled: boolean 10 | trayWindow: boolean 11 | menu: boolean 12 | tooltip: string 13 | margin: { x: number; y: number } 14 | showAtStartup: boolean 15 | } 16 | 17 | export interface AssetsPaths { 18 | config: string 19 | icon: string 20 | icon_raw: string 21 | mcp: string 22 | mcpb: string 23 | llm: string 24 | popup: string 25 | startup: string 26 | } 27 | 28 | export default class Constants { 29 | static IS_DEV_ENV = process.env.NODE_ENV === 'development' 30 | 31 | // Display app name (uppercase first letter) 32 | // static APP_NAME = name.charAt(0).toUpperCase() + name.slice(1) 33 | 34 | // Display app name (uppercase all letters) 35 | static APP_NAME = `${name.toUpperCase()}${Constants.IS_DEV_ENV ? ' ' + process.env.NODE_ENV : ''}` 36 | 37 | static APP_VERSION = version 38 | 39 | static APP_HOME_PAGE = homepage 40 | 41 | static PARTITION_NAME = `persist:${name}-${process.env.NODE_ENV}-${schemaVersion}` 42 | 43 | // To show devtools at startup. It requires IS_DEV_ENV=true. 44 | // Note: For debugging purpose, window won't be closed if click elsewhere, if devtools is open. 45 | static IS_DEVTOOLS = true 46 | 47 | static IS_MAC = process.platform === 'darwin' 48 | 49 | static DEFAULT_WEB_PREFERENCES = { 50 | nodeIntegration: false, 51 | contextIsolation: true, 52 | enableRemoteModule: false, 53 | webSecurity: false, 54 | partition: Constants.PARTITION_NAME, 55 | preload: join(__dirname, '../preload/index.js') 56 | } 57 | 58 | static DEFAULT_TRAY_OPTIONS: TrayOptions = { 59 | enabled: false, 60 | trayWindow: false, 61 | menu: false, 62 | tooltip: Constants.APP_NAME, 63 | margin: { x: 0, y: 0 }, 64 | showAtStartup: false 65 | } 66 | 67 | static APP_URL = __dirname 68 | 69 | static APP_INDEX_URL_DEV = `${debug.env.VITE_DEV_SERVER_URL}/index.html` 70 | static APP_INDEX_URL_PROD = join(__dirname, '../index.html') 71 | 72 | private static _buildAssetsPath(...paths: string[]) { 73 | const basePath = app.isPackaged ? process.resourcesPath : 'src/main' 74 | return join(basePath, 'assets', ...paths) 75 | } 76 | 77 | static ASSETS_PATH: AssetsPaths = { 78 | config: Constants._buildAssetsPath('config'), 79 | icon: Constants._buildAssetsPath('icon', 'icon.png'), 80 | icon_raw: Constants._buildAssetsPath('icon', 'icon_raw.png'), 81 | 82 | mcpb: Constants._buildAssetsPath('mcpb'), 83 | 84 | mcp: Constants._buildAssetsPath('config', 'mcp.json'), 85 | llm: Constants._buildAssetsPath('config', 'llm.json'), 86 | popup: Constants._buildAssetsPath('config', 'popup.json'), 87 | startup: Constants._buildAssetsPath('config', 'startup.json') 88 | } 89 | 90 | static getPosixPath(inputPath) { 91 | return normalize(inputPath).split(sep).join('/') 92 | } 93 | 94 | static getDxtSource( 95 | filename: string, 96 | requiredExtension: string = '.mcpb' 97 | ): { 98 | mcpbPath: string 99 | outputDir: string 100 | } { 101 | if (!filename.endsWith(requiredExtension)) { 102 | throw new Error(`File extension name must be: ${requiredExtension}`) 103 | } 104 | const dirName = filename.slice(0, -requiredExtension.length) 105 | const dirPath = join(this.ASSETS_PATH.mcpb, dirName, '/') 106 | return { 107 | mcpbPath: join(dirPath, filename), 108 | outputDir: dirPath 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/aid/commander.ts: -------------------------------------------------------------------------------- 1 | import { clipboard } from 'electron' 2 | import { showNotification } from '../utils//notification' 3 | import { putCachedText } from './utils' 4 | import Automator from './automator' 5 | import Automation from './automation' 6 | import * as window from './window' 7 | 8 | const askMeAnythingId = '00000000-0000-0000-0000-000000000000' 9 | 10 | export const notEditablePrompts = [askMeAnythingId] 11 | 12 | export default class Commander { 13 | static init = async (): Promise => { 14 | try { 15 | const _automator = new Automator() 16 | console.log('Nut Automator initiated.') 17 | return true 18 | } catch (error) { 19 | const message = error instanceof Error ? error.message : String(error) 20 | console.log(`Nut Automator cannot be initiated: ${message}`) 21 | return false 22 | } 23 | } 24 | static initCommand = async (timeout?: number): Promise => { 25 | // not available in mas 26 | if (process.mas) { 27 | window.showMasLimitsDialog() 28 | return 29 | } 30 | 31 | // start time 32 | const startTime = Date.now() 33 | 34 | // get selected text 35 | const automator = new Automator() 36 | 37 | let text = await Automation.grabSelectedText(automator, timeout) 38 | 39 | if (text == null || text.trim() === '') { 40 | text = clipboard.readText() 41 | } 42 | 43 | // error 44 | if (text == null || text.trim() === '') { 45 | showNotification({ 46 | body: 'No Text Selected' 47 | }) 48 | } 49 | 50 | // go on with a cached text id 51 | const textId = putCachedText(text) 52 | const sourceApp = await automator.getForemostApp() 53 | window.openCommandPicker({ textId, sourceApp, startTime }) 54 | } 55 | 56 | // execCommand = async (app: App, params: RunCommandParams): Promise => { 57 | 58 | // // deconstruct 59 | // const { textId, sourceApp, command } = params; 60 | 61 | // // get text 62 | // const text = getCachedText(textId); 63 | 64 | // try { 65 | 66 | // // check 67 | // if (!text) { 68 | // console.error('No text to process'); 69 | // return false; 70 | // } 71 | 72 | // // config 73 | // const config: Configuration = loadSettings(app); 74 | // const llmManager: ILlmManager = LlmFactory.manager(config); 75 | 76 | // // extract what we need 77 | // let engine = command.engine || config.commands.engine; 78 | // let model = command.model || config.commands.model; 79 | // if (!engine?.length || !model?.length) { 80 | // ({ engine, model } = llmManager.getChatEngineModel(false)); 81 | // } 82 | 83 | // // template may be localized 84 | // let template = command.template 85 | // if (!template) { 86 | // const t = useI18nLlm(app); 87 | // template = t(`commands.commands.${command.id}.template`) 88 | // } 89 | 90 | // // build prompt 91 | // const prompt = template.replace('{input}', text); 92 | 93 | // // build the params 94 | // const promptParams = { 95 | // promptId: putCachedText(prompt), 96 | // sourceApp: sourceApp, 97 | // engine: engine || command.engine, 98 | // model: model || command.model, 99 | // execute: command.id != askMeAnythingId, 100 | // action: params.action || 'default', 101 | // replace: true, 102 | // }; 103 | 104 | // // and open the window 105 | // window.openPromptAnywhere(promptParams); 106 | // return true; 107 | 108 | // } catch (error) { 109 | // console.error('Error while executing command', error); 110 | // } 111 | 112 | // // done 113 | // return false; 114 | 115 | // } 116 | } 117 | -------------------------------------------------------------------------------- /src/renderer/components/pages/McpDxtPage.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 139 | -------------------------------------------------------------------------------- /src/main/tray.ts: -------------------------------------------------------------------------------- 1 | import { app, screen, session, Menu, Tray, BrowserWindow } from 'electron' 2 | import Constants from './utils/Constants.ts' 3 | import { debounce } from './utils/Util.ts' 4 | let tray 5 | let trayOptions 6 | 7 | export function createTray(window: BrowserWindow, options) { 8 | trayOptions = options || Constants.DEFAULT_TRAY_OPTIONS 9 | // menu or trayWindow, you need to choose 10 | if (trayOptions.trayWindow) { 11 | trayOptions.menu = false 12 | } 13 | 14 | tray = new Tray(Constants.ASSETS_PATH.icon) 15 | tray.setToolTip(trayOptions.tooltip) 16 | if (trayOptions.menu) { 17 | tray.on('click', function (_event) { 18 | debounce(() => toggleWindow(window), 100) 19 | }) 20 | const contextMenu = Menu.buildFromTemplate([ 21 | { 22 | label: 'Open Dev Tools', 23 | click: () => { 24 | window.webContents.openDevTools() 25 | } 26 | }, 27 | { 28 | label: 'Force Reload', 29 | click: () => { 30 | window.webContents.reloadIgnoringCache() 31 | } 32 | }, 33 | { 34 | label: 'Clear Storage', 35 | click: () => { 36 | const sess = session.fromPartition(Constants.PARTITION_NAME) 37 | sess.clearStorageData({ 38 | storages: ['cookies', 'cachestorage', 'localstorage', 'indexdb', 'serviceworkers'] 39 | }) 40 | window.webContents.reloadIgnoringCache() 41 | } 42 | }, 43 | { 44 | label: 'Exit', 45 | click: () => { 46 | app.quit() 47 | } 48 | } 49 | ]) 50 | // tray icon only with classic window 51 | tray.setContextMenu(contextMenu) 52 | } else { 53 | // handle click on tray icon 54 | tray.on('right-click', function (_event) { 55 | debounce(() => toggleWindow(window)) 56 | }) 57 | tray.on('click', function (_event) { 58 | debounce(() => toggleWindow(window)) 59 | }) 60 | // no menu for tray window 61 | // window.setMenu(null) 62 | // tray.setContextMenu(null) 63 | } 64 | // align at startup 65 | alignWindow(window) 66 | return tray 67 | } 68 | 69 | export function hideWindow(window: BrowserWindow) { 70 | window.hide() 71 | // if (!trayOptions.trayWindow) return; 72 | // hide window when click elsewhere on screen 73 | // window.on('blur', () => { 74 | // // dont close if devtools 75 | // if (!window.webContents.isDevToolsOpened()) { 76 | // window.hide() 77 | // } 78 | // }) 79 | } 80 | 81 | export function toggleWindow(window: BrowserWindow) { 82 | if (window.isVisible()) { 83 | hideWindow(window) 84 | } else { 85 | showWindow(window) 86 | } 87 | } 88 | 89 | export function showWindow(window: BrowserWindow) { 90 | window.show() 91 | alignWindow(window) 92 | } 93 | 94 | export function alignWindow(window: BrowserWindow) { 95 | if (!trayOptions.trayWindow) return 96 | 97 | const b = window.getBounds() 98 | const position = calculateWindowPosition(b) 99 | window.setBounds({ 100 | width: b.width, 101 | height: b.height, 102 | x: position.x, 103 | y: position.y 104 | }) 105 | } 106 | 107 | function calculateWindowPosition(b) { 108 | const margin = trayOptions.margin 109 | const screenBounds = screen.getPrimaryDisplay().size 110 | const trayBounds = tray.getBounds() 111 | const bottom = trayBounds.y > screenBounds.height / 2 112 | const x = Math.floor(trayBounds.x - b.width / 2 - margin.x + trayBounds.width / 2) 113 | const y = bottom 114 | ? Math.floor(trayBounds.y - b.height - margin.y + trayBounds.height / 2) 115 | : Math.floor(trayBounds.y + margin.y + trayBounds.height / 2) 116 | // constraint into screen 117 | return { 118 | x: Math.max(0, Math.min(screenBounds.width - b.width, x)), 119 | y: Math.max(0, Math.min(screenBounds.height - b.height, y)) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/renderer/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": {}, 3 | "dxt": { 4 | "title": "MCP 扩展包", 5 | "name": "名称", 6 | "version": "版本", 7 | "description": "介绍", 8 | "author": { 9 | "name": "作者", 10 | "email": "邮件", 11 | "url": "URL" 12 | }, 13 | "License": "许可类型", 14 | "url": "主页", 15 | "type": "类型", 16 | "user-config": "用户配置", 17 | "platform": "平台", 18 | "keywords": "关键词", 19 | "repository": "仓库", 20 | "homepage": "主页", 21 | "support": "支持", 22 | "documentation": "文档", 23 | "tools": "工具列表", 24 | "required": "请提供必需的参数", 25 | "number-range": "允许的取值范围为 {min} ~ {max}" 26 | }, 27 | "elicitation": { 28 | "title": "征询", 29 | "accept": "确认", 30 | "decline": "拒绝", 31 | "string": { 32 | "too-short": "允许的最小长度为{min}", 33 | "too-long": "允许的最大长度为{max}" 34 | } 35 | }, 36 | "title": { 37 | "main": "MCP 界面", 38 | "chat": "聊天界面", 39 | "agent": "智能体界面", 40 | "setting": "设置界面", 41 | "error": "未知错误" 42 | }, 43 | "mcp": { 44 | "config": "服务端配置", 45 | "stdio": "Stdio 配置", 46 | "file": "文件压缩包", 47 | "add": "新增 MCP 服务端", 48 | "init": "启动 MCP 服务端", 49 | "stop": "停止 MCP 服务端", 50 | "search": "MCP 注册中心查询", 51 | "total": "服务端总数", 52 | "read": "阅读", 53 | "minutes": "分钟", 54 | "open": "显示配置文件", 55 | "updated": "MCP 服务端已更新" 56 | }, 57 | "menu": { 58 | "change-theme": "改变主题", 59 | "change-language": "改变语言", 60 | "increase-count": "计数 1 个增量", 61 | "documentation": "文档", 62 | "github": "源代码", 63 | "open-file": "打开文本文件" 64 | }, 65 | "locale": { 66 | "title": "语言设置" 67 | }, 68 | "chat": { 69 | "wipe": "清理对话", 70 | "reg": "重新生成", 71 | "continue": "继续生成", 72 | "reduce": "丢弃较早的消息", 73 | "token-refresh": "无效的 API key, 正在更新中...", 74 | "token-fail": "更新 API key 失败.", 75 | "select-model": "选择模型", 76 | "select-agent": "选择智能体", 77 | "delete": "删除聊天历史", 78 | "download": "下载聊天历史" 79 | }, 80 | "agent": { 81 | "config": "智能体配置", 82 | "prompt": "提示指令", 83 | "tools": "工具列表", 84 | "no-tools": "尚未连接包含工具的 MCP 服务端", 85 | "all": "全部工具", 86 | "selected": "已选择工具", 87 | "cancel": "取消", 88 | "save": "保存", 89 | "add": "新增智能体", 90 | "reset": "重置智能体" 91 | }, 92 | "prompt": { 93 | "title": "服务端提示模板", 94 | "get": "获取" 95 | }, 96 | "resource": { 97 | "list": "资源列表", 98 | "total": "资源总数" 99 | }, 100 | "sampling": { 101 | "title": "采样", 102 | "comp": "开始采样", 103 | "pause": "暂停", 104 | "continue": "继续", 105 | "reject": "拒绝采样", 106 | "confirm": "确认使用采样", 107 | "confirm-last": "确认使用最新采样" 108 | }, 109 | "setting": { 110 | "title-api": "API 设置", 111 | "title-model": "模型参数", 112 | "name": "名称", 113 | "apikey": "API 密钥", 114 | "dialog": "命令行调用", 115 | "exec": "执行", 116 | "url": "URL", 117 | "path": "Path", 118 | "model": "模型", 119 | "advanced": "Advanced", 120 | "method": "HTTP Method", 121 | "stream": "Stream", 122 | "mcp": "MCP工具", 123 | "max-tokens-prefix": "Max Tokens Prefix", 124 | "temperature": "温度", 125 | "topP": "核采样", 126 | "enable-thinking": "思考模式", 127 | "reasoning-effort": "推理负荷", 128 | "auth-header": "鉴权头", 129 | "auth-prefix": "鉴权前缀", 130 | "config-file": "加载配置文件", 131 | "add": "新增配置", 132 | "reset": "重置配置" 133 | }, 134 | "validation": { 135 | "invalid-number": "必须为有效数字", 136 | "number-range": "数值必须在 {min} 到 {max} 之间" 137 | }, 138 | "snackbar": { 139 | "addnew": "开始新会话", 140 | "addfail": "已处于最新会话", 141 | "no-mcp-config": "请提供一个含有名字的 MCP JSON 配置", 142 | "stopped": "停止生成", 143 | "parse-stream-fail": "无法解析数据流", 144 | "parse-config-fail": "无法解析配置文件", 145 | "copied": "已复制到剪切板" 146 | }, 147 | "general": { 148 | "edit": "修改", 149 | "example": "示例", 150 | "delete": "删除", 151 | "reset": "重置" 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/renderer/store/prompt.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { useMcpStore } from '@/renderer/store/mcp' 3 | import { getServers } from './mcp' 4 | 5 | import type { Prompt as PromptType, GetPromptRequest } from '@modelcontextprotocol/sdk/types.d.ts' 6 | 7 | type ParamsType = GetPromptRequest['params'] 8 | 9 | // Extend the argument content to be sent to the MCP server 10 | export type ExtendedArgument = NonNullable[number] & { 11 | content?: string 12 | } 13 | 14 | type ExtendedPromptType = Omit & { 15 | arguments?: ExtendedArgument[] 16 | } 17 | 18 | export const usePromptStore = defineStore('promptStore', { 19 | state: () => ({ 20 | promptDialog: false, 21 | promptSheet: false, 22 | promptList: [] as ExtendedPromptType[], 23 | promptSelect: {} as ExtendedPromptType, 24 | search: '', 25 | loading: false 26 | }), 27 | actions: { 28 | loadPrompts: function () { 29 | this.loading = true 30 | try { 31 | this.fetchPrompts().then((prompts) => { 32 | console.log(prompts) 33 | this.promptList = prompts 34 | }) 35 | } catch (error) { 36 | console.error('Failed to load prompts:', error) 37 | } finally { 38 | this.loading = false 39 | } 40 | }, 41 | fetchPrompts: async function () { 42 | const mcpStore = useMcpStore() 43 | const mcpServers = mcpStore.getSelected 44 | const prompts = await mcpServers.method.list() 45 | return prompts.prompts.map((prompt: PromptType) => ({ 46 | title: mcpServers.server, 47 | ...prompt 48 | })) 49 | }, 50 | fetchAllPrompts: async function () { 51 | const mcpServers = getServers() 52 | if (!mcpServers) { 53 | return [] 54 | } 55 | const mcpKeys = Object.keys(mcpServers) 56 | const allPrompts = [] as PromptType[] 57 | for (const key of mcpKeys) { 58 | const func = mcpServers[key]?.prompts?.list 59 | 60 | if (func) { 61 | try { 62 | const obj = await func({ method: 'prompts/list' }) 63 | if (obj) { 64 | obj.prompts.forEach((prompt) => allPrompts.push({ title: key, ...prompt })) 65 | } 66 | } catch (error) { 67 | console.error(`Error fetching prompts from ${key}:`, error) 68 | } 69 | } 70 | } 71 | 72 | return allPrompts 73 | }, 74 | select: function (prompt: PromptType) { 75 | console.log(prompt.title, prompt.name, prompt.arguments) 76 | this.promptSelect = prompt 77 | this.promptSheet = true 78 | this.promptDialog = false 79 | }, 80 | fetchSelect: async function () { 81 | const mcpStore = useMcpStore() 82 | const mcpServers = getServers() 83 | const title = this.promptSelect.title 84 | if (!title) { 85 | return [] 86 | } 87 | const getFun = mcpServers?.[title]?.prompts?.get 88 | if (!getFun) { 89 | return [] 90 | } 91 | const params: ParamsType = { 92 | name: this.promptSelect.name 93 | } 94 | if (this.promptSelect.arguments) { 95 | for (const argument of this.promptSelect.arguments) { 96 | if (argument.name) { 97 | if (!params.arguments) { 98 | params.arguments = {} 99 | } 100 | params.arguments[argument.name] = argument.content as string 101 | } 102 | } 103 | } 104 | 105 | console.log(params) 106 | const prompts = await getFun({ method: 'prompts/get', params }) 107 | 108 | const conversations = prompts.messages.map((item) => { 109 | const content = mcpStore.convertItem(item.content) 110 | const conversation = { 111 | role: item.role, 112 | content: item.role === 'user' ? [content] : content.text 113 | } 114 | return conversation 115 | }) 116 | 117 | this.promptSheet = false 118 | 119 | return conversations 120 | } 121 | } 122 | }) 123 | -------------------------------------------------------------------------------- /src/renderer/store/chatbot.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { CHATBOT_DEFAULTS } from '@/renderer/types' 3 | import type { ChatbotConfig } from '@/types/llm' 4 | import { v4 as uuidv4 } from 'uuid' 5 | 6 | export interface ChatbotStoreState { 7 | chatbots: ChatbotConfig[] 8 | currentChatbotId: number // chatbots array index, being modified 9 | selectedChatbotId: number // chatbots array index, being selected 10 | } 11 | 12 | export function getLLMs(): ChatbotConfig[] { 13 | return window.llmApis?.get().custom || [] 14 | } 15 | 16 | export function getDefaultLLM(): ChatbotConfig { 17 | return window.llmApis?.get().default || CHATBOT_DEFAULTS 18 | } 19 | 20 | export const useChatbotStore = defineStore('chatbotStore', { 21 | state: (): ChatbotStoreState => { 22 | const llms = getLLMs() 23 | 24 | const chatbots = llms 25 | ? llms.map((llm) => ({ ...getDefaultLLM(), ...llm })) 26 | : [{ ...getDefaultLLM(), name: 'Chatbot Default', mcp: true }] 27 | 28 | return { 29 | chatbots: chatbots, 30 | currentChatbotId: 0, // points to first chatbot by default 31 | selectedChatbotId: 0 32 | } 33 | }, 34 | 35 | persist: { 36 | exclude: ['currentChatbotId'] 37 | }, 38 | 39 | actions: { 40 | resetState() { 41 | this.$reset() 42 | }, 43 | 44 | updateChatbotConfig(index: number, patch: Partial) { 45 | if (index < 0 || index >= this.chatbots.length) { 46 | console.log('No chatbot found at index', index) 47 | return 48 | } 49 | Object.assign(this.chatbots[index], patch) 50 | if ((this.chatbots[index].apiCli, 'apiKey' in patch)) { 51 | } 52 | }, 53 | 54 | batchChatbotApiKey(apiCli: string, apiKey: string) { 55 | this.chatbots.forEach((chatbot: ChatbotConfig) => { 56 | if (chatbot.apiCli.length > 0 && chatbot.apiCli === apiCli) { 57 | chatbot.apiKey = apiKey 58 | } 59 | }) 60 | }, 61 | 62 | addChatbot() { 63 | this.chatbots.push({ ...getDefaultLLM(), name: 'Chatbot ' + uuidv4() }) 64 | }, 65 | 66 | removeChatbot(index: number) { 67 | this.chatbots.splice(index, 1) 68 | // Adjust current index if needed 69 | if (this.currentChatbotId >= index) { 70 | this.currentChatbotId = Math.max(0, this.currentChatbotId - 1) 71 | } 72 | }, 73 | 74 | updateStoreFromJSON(chatbotConfigJson: ChatbotConfig[] | ChatbotConfig) { 75 | this.$reset() 76 | this.chatbots = [] 77 | if (Array.isArray(chatbotConfigJson)) { 78 | chatbotConfigJson.forEach((newChatbot, _index) => { 79 | this.chatbots.push({ ...getDefaultLLM(), ...newChatbot }) 80 | }) 81 | } else { 82 | // Handle case when chatbots is a single object 83 | this.chatbots.push({ ...getDefaultLLM(), ...chatbotConfigJson }) 84 | } 85 | }, 86 | 87 | // Helper method to find chatbot by name 88 | findChatbotIndexByName(name: string): number { 89 | return this.chatbots.findIndex((chatbot) => chatbot.name === name) 90 | }, 91 | 92 | // Action to set current chatbot by name 93 | setCurrentChatbotByName(name: string) { 94 | const index = this.findChatbotIndexByName(name) 95 | if (index !== -1) { 96 | this.currentChatbotId = index 97 | } 98 | } 99 | }, 100 | 101 | getters: { 102 | currentConfig(state): ChatbotConfig | null { 103 | if (state.currentChatbotId < 0 || state.currentChatbotId >= state.chatbots.length) { 104 | return null 105 | } 106 | return state.chatbots[state.currentChatbotId] 107 | }, 108 | 109 | getChatbotByName: (state) => { 110 | return (name: string) => state.chatbots.find((chatbot) => chatbot.name === name) 111 | }, 112 | 113 | getChatbotByIndex: (state) => { 114 | return (index: number) => state.chatbots[index] 115 | }, 116 | 117 | // Get all chatbot names 118 | chatbotNames(state): string[] { 119 | return state.chatbots.map((chatbot) => chatbot.name) 120 | } 121 | } 122 | }) 123 | -------------------------------------------------------------------------------- /docs/src/en/installation-and-build/build-configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 2 3 | --- 4 | 5 | # Build Configurations 6 | 7 | Once the module installation is complete, you can simply build the platform package with the command below. 8 | 9 | ```shell 10 | # For Windows (.exe, .appx) 11 | $ npm run build:win 12 | 13 | # For macOS (.dmg) 14 | $ npm run build:mac 15 | 16 | # For Linux (.rpm, .deb, .snap) 17 | $ npm run build:linux 18 | 19 | # All platform (.exe, .appx, .dmg, .rpm, .deb, .snap) - see below description 20 | $ npm run build:all 21 | ``` 22 | 23 | The built packages can be found in `release/{version}` location. 24 | 25 | For more information, please refer to the following article: https://webpack.electron.build/dependency-management#installing-native-node-modules 26 | 27 | ## What do I need to do for a multi-platform build? 28 | 29 | To create a package for each OS, you must build it on the same OS. For example, a package for macOS must be built on a macOS machine. 30 | 31 | However, you can build packages for Windows, macOS, and Linux all at once on one OS. However, this might require some preparation. 32 | 33 | **macOS** is recommended if you want to build multiple platforms simultaneously on one platform. Because it can be configured with just a few very simple settings. 34 | 35 | You can perform multi-platform builds at once with the following command. Alternatively, you can just do it for the OS you want via the individual build commands above. 36 | 37 | ```shell 38 | $ npm run build:all 39 | ``` 40 | 41 | `Multipass` configuration may be required for Linux builds. Learn more about `Multipass` through the following link: https://multipass.run 42 | 43 | To learn more about multiplatform builds, see the following articles: https://electron.build/multi-platform-build 44 | 45 | ## Reduce bundle size by excluding development files 46 | 47 | You can exclude files you don't need at build time by adding a file pattern to the files property of `buildAssets/builder/config.ts`. This will save bundle capacity. 48 | 49 | Below is an unnecessary `node_modules` file pattern that can further save bundles. Depending on the project, using the rules below may cause problems, so please review it before using. 50 | 51 | ```json 52 | [ 53 | "!**/.*", 54 | "!**/node_modules/**/{CONTRIBUTORS,CNAME,AUTHOR,TODO,CONTRIBUTING,COPYING,INSTALL,NEWS,PORTING,Makefile,htdocs,CHANGELOG,ChangeLog,changelog,README,Readme,readme,test,sample,example,demo,composer.json,tsconfig.json,jsdoc.json,tslint.json,typings.json,gulpfile,bower.json,package-lock,Gruntfile,CMakeLists,karma.conf,yarn.lock}*", 55 | "!**/node_modules/**/{man,benchmark,node_modules,spec,cmake,browser,vagrant,doxy*,bin,obj,obj.target,example,examples,test,tests,doc,docs,msvc,Xcode,CVS,RCS,SCCS}{,/**/*}", 56 | "!**/node_modules/**/*.{conf,png,pc,coffee,txt,spec.js,ts,js.flow,html,def,jst,xml,ico,in,ac,sln,dsp,dsw,cmd,vcproj,vcxproj,vcxproj.filters,pdb,exp,obj,lib,map,md,sh,gypi,gyp,h,cpp,yml,log,tlog,Makefile,mk,c,cc,rc,xcodeproj,xcconfig,d.ts,yaml,hpp}", 57 | "!**/node_modules/**/node-v*-x64{,/**/*}", 58 | "!**/node_modules/bluebird/js/browser{,/**/*}", 59 | "!**/node_modules/bluebird/js/browser{,/**/*}", 60 | "!**/node_modules/source-map/dist{,/**/*}", 61 | "!**/node_modules/lodash/fp{,/**/*}", 62 | "!**/node_modules/async/!(dist|package.json)", 63 | "!**/node_modules/async/internal{,/**/*}", 64 | "!**/node_modules/ajv/dist{,/**/*}", 65 | "!**/node_modules/ajv/scripts{,/**/*}", 66 | "!**/node_modules/node-pre-gyp/!(lib|package.json)", 67 | "!**/node_modules/node-pre-gyp/lib/!(util|pre-binding.js|node-pre-gyp.js)", 68 | "!**/node_modules/node-pre-gyp/lib/util/!(versioning.js|abi_crosswalk.json)", 69 | "!**/node_modules/source-map-support/browser-source-map-support.js", 70 | "!**/node_modules/json-schema/!(package.json|lib)" 71 | ] 72 | ``` 73 | 74 | ## Build settings for projects that use Native Node modules 75 | 76 | For projects that use the **Native Node Module**, add the following script to your `package.json`: When installing dependencies, `electron-builder` will take care of any modules that require rebuilding. 77 | 78 | ```json 79 | { 80 | "scripts": { 81 | "postinstall": "electron-builder install-app-deps" 82 | } 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /src/renderer/store/resource.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { useMcpStore } from '@/renderer/store/mcp' 3 | 4 | import { ListResourcesResult, ListResourceTemplatesResult } from '@modelcontextprotocol/sdk/types' 5 | 6 | type ResourceTemplatesType = ListResourceTemplatesResult['resourceTemplates'] 7 | 8 | type ResourcesType = ListResourcesResult['resources'] 9 | 10 | type CursorType = ListResourcesResult['nextCursor'] 11 | 12 | type ResourceRecordType = { 13 | resource: ResourceItemType 14 | template: ResourceTemplateType 15 | } 16 | 17 | type ResourceItemType = { 18 | perPage: number 19 | cursor: CursorType 20 | list: ResourcesType 21 | } 22 | 23 | type ResourceTemplateType = { 24 | perPage: number 25 | cursor: CursorType 26 | list: ResourceTemplatesType 27 | } 28 | 29 | export const emptyItem: ResourceItemType | ResourceTemplateType = { 30 | perPage: -1, 31 | cursor: undefined, 32 | list: [] 33 | } 34 | 35 | export const useResourceStore = defineStore('resourceStore', { 36 | state: () => ({ 37 | data: {} as Record, 38 | loadingTemplates: false, 39 | loadingResources: false 40 | }), 41 | actions: { 42 | loadTemplates: function (serverName: string) { 43 | this.loadingTemplates = true 44 | const mcpStore = useMcpStore() 45 | const resourceFunction = mcpStore.getServerFunction({ 46 | serverName, 47 | primitiveName: 'resources', 48 | methodName: 'templates/list' 49 | }) 50 | try { 51 | resourceFunction().then((result: ListResourceTemplatesResult) => { 52 | console.log(result) 53 | this.data[serverName] = this.data[serverName] || {} 54 | this.data[serverName].template = { 55 | perPage: -1, 56 | cursor: result.nextCursor, 57 | list: result.resourceTemplates 58 | } 59 | }) 60 | } catch (error) { 61 | console.error('Failed to load resource templates:', error) 62 | } finally { 63 | this.loadingTemplates = false 64 | } 65 | }, 66 | loadResources: async function (serverName: string) { 67 | // Already loaded 68 | if (this.data[serverName] && 'resource' in this.data[serverName]) return 69 | 70 | this.loadingResources = true 71 | const mcpStore = useMcpStore() 72 | const resourceFunction = mcpStore.getServerFunction({ 73 | serverName, 74 | primitiveName: 'resources', 75 | methodName: 'list' 76 | }) 77 | 78 | try { 79 | const result: ListResourcesResult = await resourceFunction() 80 | console.log(result) 81 | const resources: ResourcesType = result.resources 82 | this.data[serverName] = this.data[serverName] || {} 83 | this.data[serverName].resource = { 84 | perPage: resources.length, 85 | cursor: result.nextCursor, 86 | list: resources 87 | } 88 | } catch (error) { 89 | console.error('Failed to load resources:', error) 90 | } finally { 91 | this.loadingResources = false 92 | } 93 | }, 94 | getNextpage: async function (serverName: string) { 95 | if (!this.data[serverName]?.resource?.cursor) { 96 | return 97 | } 98 | this.loadingResources = true 99 | const currentResource = this.data[serverName].resource 100 | 101 | try { 102 | const mcpStore = useMcpStore() 103 | const resourceFunction = mcpStore.getServerFunction({ 104 | serverName, 105 | primitiveName: 'resources', 106 | methodName: 'list' 107 | }) 108 | 109 | const result: ListResourcesResult = await resourceFunction({ 110 | method: 'resources/list', 111 | params: { 112 | cursor: currentResource.cursor 113 | } 114 | }) 115 | 116 | console.log(result) 117 | const resources: ResourcesType = result.resources 118 | 119 | currentResource.list = [...currentResource.list, ...resources] 120 | currentResource.cursor = result.nextCursor 121 | } catch (error) { 122 | console.error('Failed to load resources:', error) 123 | } finally { 124 | this.loadingResources = false 125 | } 126 | } 127 | } 128 | }) 129 | --------------------------------------------------------------------------------