Compare commits
16 Commits
copilot/fi
...
fix/thinki
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49320936f8 | ||
|
|
e268e69597 | ||
|
|
10e78ac60e | ||
|
|
44b2b859da | ||
|
|
bfef0c5580 | ||
|
|
1e8055031a | ||
|
|
8e33ff8d90 | ||
|
|
a619000340 | ||
|
|
78278ce96d | ||
|
|
76483d828e | ||
|
|
816a92c609 | ||
|
|
83e4d4363f | ||
|
|
1103449a4f | ||
|
|
56c7a7f066 | ||
|
|
caa59c4c50 | ||
|
|
2546dfbe5d |
87
.github/workflows/auto-i18n.yml
vendored
87
.github/workflows/auto-i18n.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Auto I18N
|
||||
name: Auto I18N Weekly
|
||||
|
||||
env:
|
||||
TRANSLATION_API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
|
||||
@@ -7,14 +7,15 @@ env:
|
||||
TRANSLATION_BASE_LOCALE: ${{ vars.AUTO_I18N_BASE_LOCALE || 'en-us'}}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
schedule:
|
||||
# Runs at 00:00 UTC every Sunday.
|
||||
# This corresponds to 08:00 AM UTC+8 (Beijing time) every Sunday.
|
||||
- cron: "0 0 * * 0"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
auto-i18n:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == 'CherryHQ/cherry-studio'
|
||||
name: Auto I18N
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -24,45 +25,69 @@ jobs:
|
||||
- name: 🐈⬛ Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 📦 Setting Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
package-manager-cache: false
|
||||
node-version: 22
|
||||
|
||||
- name: 📦 Install dependencies in isolated directory
|
||||
- name: 📦 Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: 📂 Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 💾 Cache yarn dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
node_modules
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: 📦 Install dependencies
|
||||
run: |
|
||||
# 在临时目录安装依赖
|
||||
mkdir -p /tmp/translation-deps
|
||||
cd /tmp/translation-deps
|
||||
echo '{"dependencies": {"@cherrystudio/openai": "^6.5.0", "cli-progress": "^3.12.0", "tsx": "^4.20.3", "@biomejs/biome": "2.2.4"}}' > package.json
|
||||
npm install --no-package-lock
|
||||
|
||||
# 设置 NODE_PATH 让项目能找到这些依赖
|
||||
echo "NODE_PATH=/tmp/translation-deps/node_modules" >> $GITHUB_ENV
|
||||
yarn install
|
||||
|
||||
- name: 🏃♀️ Translate
|
||||
run: npx tsx scripts/sync-i18n.ts && npx tsx scripts/auto-translate-i18n.ts
|
||||
run: yarn sync:i18n && yarn auto:i18n
|
||||
|
||||
- name: 🔍 Format
|
||||
run: cd /tmp/translation-deps && npx biome format --config-path /home/runner/work/cherry-studio/cherry-studio/biome.jsonc --write /home/runner/work/cherry-studio/cherry-studio/src/renderer/src/i18n/
|
||||
run: yarn format
|
||||
|
||||
- name: 🔄 Commit changes
|
||||
- name: 🔍 Check for changes
|
||||
id: git_status
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git add .
|
||||
# Check if there are any uncommitted changes
|
||||
git reset -- package.json yarn.lock # 不提交 package.json 和 yarn.lock 的更改
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "fix(i18n): Auto update translations for PR #${{ github.event.pull_request.number }}"
|
||||
fi
|
||||
git diff --exit-code --quiet || echo "::set-output name=has_changes::true"
|
||||
git status --porcelain
|
||||
|
||||
- name: 🚀 Push changes
|
||||
uses: ad-m/github-push-action@master
|
||||
- name: 📅 Set current date for PR title
|
||||
id: set_date
|
||||
run: echo "CURRENT_DATE=$(date +'%b %d, %Y')" >> $GITHUB_ENV # e.g., "Jun 06, 2024"
|
||||
|
||||
- name: 🚀 Create Pull Request if changes exist
|
||||
if: steps.git_status.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: ${{ github.event.pull_request.head.ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }} # Use the built-in GITHUB_TOKEN for bot actions
|
||||
commit-message: "feat(bot): Weekly automated script run"
|
||||
title: "🤖 Weekly Automated Update: ${{ env.CURRENT_DATE }}"
|
||||
body: |
|
||||
This PR includes changes generated by the weekly auto i18n.
|
||||
Review the changes before merging.
|
||||
|
||||
---
|
||||
_Generated by the automated weekly workflow_
|
||||
branch: "auto-i18n-weekly-${{ github.run_id }}" # Unique branch name
|
||||
base: "main" # Or 'develop', set your base branch
|
||||
delete-branch: true # Delete the branch after merging or closing the PR
|
||||
|
||||
- name: 📢 Notify if no changes
|
||||
if: steps.git_status.outputs.has_changes != 'true'
|
||||
run: echo "Bot script ran, but no changes were detected. No PR created."
|
||||
|
||||
6
.github/workflows/github-issue-tracker.yml
vendored
6
.github/workflows/github-issue-tracker.yml
vendored
@@ -5,7 +5,7 @@ on:
|
||||
types: [opened]
|
||||
schedule:
|
||||
# Run every day at 8:30 Beijing Time (00:30 UTC)
|
||||
- cron: '30 0 * * *'
|
||||
- cron: "30 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
if: steps.check_time.outputs.should_delay == 'false'
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: 22
|
||||
|
||||
- name: Process issue with Claude
|
||||
if: steps.check_time.outputs.should_delay == 'false'
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: 22
|
||||
|
||||
- name: Process pending issues with Claude
|
||||
uses: anthropics/claude-code-action@main
|
||||
|
||||
6
.github/workflows/nightly-build.yml
vendored
6
.github/workflows/nightly-build.yml
vendored
@@ -3,7 +3,7 @@ name: Nightly Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 17 * * *' # 1:00 BJ Time
|
||||
- cron: "0 17 * * *" # 1:00 BJ Time
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- name: macos-latest dependencies fix
|
||||
if: matrix.os == 'macos-latest'
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
brew install python-setuptools
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
|
||||
4
.github/workflows/pr-ci.yml
vendored
4
.github/workflows/pr-ci.yml
vendored
@@ -26,10 +26,10 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
|
||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -4,9 +4,9 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag (e.g. v1.0.0)'
|
||||
description: "Release tag (e.g. v1.0.0)"
|
||||
required: true
|
||||
default: 'v1.0.0'
|
||||
default: "v1.0.0"
|
||||
push:
|
||||
tags:
|
||||
- v*.*.*
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- name: macos-latest dependencies fix
|
||||
if: matrix.os == 'macos-latest'
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
brew install python-setuptools
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
run: corepack enable && corepack prepare yarn@4.9.1 --activate
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
@@ -127,5 +127,5 @@ jobs:
|
||||
allowUpdates: true
|
||||
makeLatest: false
|
||||
tag: ${{ steps.get-tag.outputs.tag }}
|
||||
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/beta*.yml,dist/*.blockmap'
|
||||
artifacts: "dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/rc*.yml,dist/beta*.yml,dist/*.blockmap"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -7,7 +7,6 @@ This file provides guidance to AI coding assistants when working with code in th
|
||||
- **Keep it clear**: Write code that is easy to read, maintain, and explain.
|
||||
- **Match the house style**: Reuse existing patterns, naming, and conventions.
|
||||
- **Search smart**: Prefer `ast-grep` for semantic queries; fall back to `rg`/`grep` when needed.
|
||||
- **Build with HeroUI**: Use HeroUI for every new UI component; never add `antd` or `styled-components`.
|
||||
- **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`.
|
||||
- **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references.
|
||||
- **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications.
|
||||
@@ -41,7 +40,6 @@ This file provides guidance to AI coding assistants when working with code in th
|
||||
- **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
|
||||
- **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces.
|
||||
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
|
||||
- **UI Components**: HeroUI (`@heroui/*`) for all new UI elements.
|
||||
|
||||
### Logging
|
||||
```typescript
|
||||
|
||||
@@ -18,13 +18,13 @@ yarn
|
||||
|
||||
### Setup Node.js
|
||||
|
||||
Download and install [Node.js v20.x.x](https://nodejs.org/en/download)
|
||||
Download and install [Node.js v22.x.x](https://nodejs.org/en/download)
|
||||
|
||||
### Setup Yarn
|
||||
|
||||
```bash
|
||||
corepack enable
|
||||
corepack prepare yarn@4.6.0 --activate
|
||||
corepack prepare yarn@4.9.1 --activate
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
@@ -11,6 +11,8 @@ The Test Plan is divided into the RC channel and the Beta channel, with the foll
|
||||
|
||||
Users can enable the "Test Plan" and select the version channel in the software's `Settings` > `About`. Please note that the versions in the "Test Plan" cannot guarantee data consistency, so be sure to back up your data before using them.
|
||||
|
||||
After enabling the RC channel or Beta channel, if a stable version is released, users will still be upgraded to the stable version.
|
||||
|
||||
Users are welcome to submit issues or provide feedback through other channels for any bugs encountered during testing. Your feedback is very important to us.
|
||||
|
||||
## Developer Guide
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
用户可以在软件的`设置`-`关于`中,开启“测试计划”并选择版本通道。请注意“测试计划”的版本无法保证数据的一致性,请使用前一定要备份数据。
|
||||
|
||||
用户选择RC版通道或Beta版通道后,若发布了正式版,仍旧会升级到正式版。
|
||||
|
||||
用户在测试过程中发现的BUG,欢迎提交issue或通过其他渠道反馈。用户的反馈对我们非常重要。
|
||||
|
||||
## 开发者指南
|
||||
|
||||
@@ -135,128 +135,59 @@ artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
<!--LANG:en-->
|
||||
What's New in v1.7.0-beta.3
|
||||
What's New in v1.7.0-beta.4
|
||||
|
||||
Major Changes:
|
||||
- UI Framework Upgrade: Improved performance and user experience with new design system
|
||||
- App Menu i18n: Menu now supports multiple languages and syncs with app language settings
|
||||
|
||||
New Features:
|
||||
- Enhanced Tool Permission System: Real-time tool approval interface with improved UX
|
||||
- Plugin Management System: Support for Claude Agent plugins (agents, commands, skills)
|
||||
- Skill Tool: Add skill execution capabilities for agents
|
||||
- Mobile App Data Restore: Support restoring data to mobile applications
|
||||
- OpenMinerU Preprocessor: Knowledge base now supports open-source MinerU for document processing
|
||||
- HuggingFace Provider: Added HuggingFace as AI provider
|
||||
- Claude Haiku 4.5: Support for the latest Claude Haiku 4.5 model
|
||||
- Ling Series Models: Added support for Ling-1T and related models
|
||||
- Intel OVMS Painting: New painting provider using Intel OpenVINO Model Server
|
||||
- Automatic Update Checks: Implement periodic update checking with configurable intervals
|
||||
- HuggingChat Mini App: New mini app for HuggingChat integration
|
||||
- AWS Bedrock API Key: Support Bedrock API key authentication with Extended Thinking (reasoning) capability
|
||||
- SophNet Provider: Added support for SophNet LLM provider
|
||||
- Auto Session Rename: Agent sessions automatically rename based on conversation topics
|
||||
- TopP Parameter: Added TopP parameter support for more precise model control
|
||||
- Reasoning Effort Control: Quick access to reasoning effort settings in input bar
|
||||
|
||||
Improvements:
|
||||
- Agent Creation: New agents are now automatically activated upon creation
|
||||
- Lazy Loading: Optimize page load performance with route lazy loading
|
||||
- UI Enhancements: Improved agent item styling and layout consistency
|
||||
- Navigation: Better navbar layout for fullscreen mode on macOS
|
||||
- Settings Tab: Enhanced context slider consistency
|
||||
- Backup Manager: Unified footer layout for local and S3 backup managers
|
||||
- Menu System: Enhanced application menu with improved help section
|
||||
- Proxy Rules: Comprehensive proxy bypass rule matching
|
||||
- German Language: Added German language support
|
||||
- MCP Confirmation: Added confirmation modal when activating protocol-installed MCP servers
|
||||
- Translation: Enhanced translation script with concurrency and validation
|
||||
- Electron & Vite: Updated to Electron 38 and Vite 4.0.1
|
||||
- QR Code Generation: Optimized performance for phone LAN export
|
||||
- Enterprise Settings: Added enterprise section in About settings
|
||||
- Assistant/Agent Popup: Enhanced UI for adding assistants and agents
|
||||
|
||||
Claude Code Tool Improvements:
|
||||
- GlobTool: Now counts lines instead of files in output for better clarity
|
||||
- ReadTool: Automatically removes system reminder tags from output
|
||||
- TodoWriteTool: Improved rendering behavior
|
||||
- Environment Variables: Updated model-related environment variable names
|
||||
- Topics & Sessions: Enhanced UI with better styling and smoother interactions
|
||||
- Quick Panel: Improved option visibility and control
|
||||
- Painting Models: Smarter model initialization with better defaults
|
||||
- System Shutdown: Better handling of shutdown events to prevent data loss
|
||||
- Smaller Package Size: Optimized build configuration for faster downloads
|
||||
|
||||
Bug Fixes:
|
||||
- Fixed session model not being used when sending messages
|
||||
- Fixed tool approval UI and shared workspace plugin inconsistencies
|
||||
- Fixed API server readiness notification to renderer
|
||||
- Fixed grouped items not respecting saved tag order
|
||||
- Fixed assistant/agent activation when creating new ones
|
||||
- Fixed Dashscope Anthropic API host and migrated old configs
|
||||
- Fixed Qwen3 thinking mode control for Ollama
|
||||
- Fixed disappeared MCP button
|
||||
- Fixed create assistant causing blank screen
|
||||
- Fixed up-down button visibility in some cases
|
||||
- Fixed hooks preventing save on composing enter key
|
||||
- Fixed Azure GPT-image-1 and OpenRouter Gemini-image
|
||||
- Fixed Silicon reasoning issues
|
||||
- Fixed topic branch incomplete copy with two-pass ID mapping
|
||||
- Fixed deep research model search context restrictions
|
||||
- Fixed model capability checking logic
|
||||
- Fixed reranker API error response capture
|
||||
- Fixed right-click paste file content into inputbar
|
||||
- Fixed minimax-m2 support in aiCore
|
||||
- Fixed Azure embedding issues
|
||||
- Fixed agent edit modal loading race condition
|
||||
- Fixed debounced save cancellation on file path update
|
||||
- Fixed Perplexity provider support and API host formatting
|
||||
- Fixed CherryAI provider support and API host formatting
|
||||
- Fixed i18n translations for painting image size options
|
||||
- Fixed agent session message token usage tracking
|
||||
- Fixed prompt stream handling on completion or error
|
||||
- Fixed message API initialization issues
|
||||
|
||||
<!--LANG:zh-CN-->
|
||||
v1.7.0-beta.3 新特性
|
||||
v1.7.0-beta.4 新特性
|
||||
|
||||
重大变更:
|
||||
- UI 框架升级:采用新设计系统,提升性能和用户体验
|
||||
- 应用菜单国际化:菜单支持多语言,并自动同步应用语言设置
|
||||
|
||||
新功能:
|
||||
- 增强工具权限系统:实时工具审批界面,改进用户体验
|
||||
- 插件管理系统:支持 Claude Agent 插件(agents、commands、skills)
|
||||
- 技能工具:为 Agent 添加技能执行能力
|
||||
- 移动应用数据恢复:支持将数据恢复到移动应用程序
|
||||
- OpenMinerU 预处理器:知识库现支持使用开源 MinerU 处理文档
|
||||
- HuggingFace 提供商:添加 HuggingFace 作为 AI 提供商
|
||||
- Claude Haiku 4.5:支持最新的 Claude Haiku 4.5 模型
|
||||
- Ling 系列模型:添加 Ling-1T 及相关模型支持
|
||||
- Intel OVMS 绘图:使用 Intel OpenVINO 模型服务器的新绘图提供商
|
||||
- 自动更新检查:实现可配置间隔的定期更新检查
|
||||
- HuggingChat 小程序:新增 HuggingChat 集成小程序
|
||||
- AWS Bedrock API 密钥:支持 Bedrock API 密钥身份验证,并支持扩展思考(推理)能力
|
||||
- SophNet 提供商:添加 SophNet LLM 提供商支持
|
||||
- 自动会话重命名:Agent 会话根据对话主题自动重命名
|
||||
- TopP 参数:添加 TopP 参数支持,更精确控制模型输出
|
||||
|
||||
改进:
|
||||
- Agent 创建:新创建的 Agent 现在会自动激活
|
||||
- 懒加载:通过路由懒加载优化页面加载性能
|
||||
- UI 增强:改进 Agent 项目样式和布局一致性
|
||||
- 导航:改进 macOS 全屏模式下的导航栏布局
|
||||
- 设置选项卡:增强上下文滑块一致性
|
||||
- 备份管理器:统一本地和 S3 备份管理器的页脚布局
|
||||
- 菜单系统:增强应用菜单,改进帮助部分
|
||||
- 代理规则:全面的代理绕过规则匹配
|
||||
- 德语支持:添加德语语言支持
|
||||
- MCP 确认:添加激活协议安装的 MCP 服务器时的确认模态框
|
||||
- 翻译:增强翻译脚本的并发和验证功能
|
||||
- Electron & Vite:更新至 Electron 38 和 Vite 4.0.1
|
||||
- 二维码生成:优化手机局域网导出性能
|
||||
- 企业设置:在关于设置中添加企业部分
|
||||
- 助手/Agent 弹窗:增强添加助手和 Agent 的界面
|
||||
|
||||
Claude Code 工具改进:
|
||||
- GlobTool:现在计算行数而不是文件数,提供更清晰的输出
|
||||
- ReadTool:自动从输出中移除系统提醒标签
|
||||
- TodoWriteTool:改进渲染行为
|
||||
- 环境变量:更新模型相关的环境变量名称
|
||||
- 主题和会话:增强 UI,改进样式和交互体验
|
||||
- 快速面板:改进选项可见性和控制
|
||||
- 绘图模型:更智能的模型初始化和更好的默认值
|
||||
- 系统关机:更好地处理关机事件,防止数据丢失
|
||||
- 更小的安装包:优化构建配置,下载更快
|
||||
|
||||
问题修复:
|
||||
- 修复发送消息时未使用会话模型
|
||||
- 修复工具审批 UI 和共享工作区插件不一致
|
||||
- 修复 API 服务器就绪通知到渲染器
|
||||
- 修复分组项目不遵守已保存标签顺序
|
||||
- 修复创建新的助手/Agent 时的激活问题
|
||||
- 修复 Dashscope Anthropic API 主机并迁移旧配置
|
||||
- 修复 Ollama 的 Qwen3 思考模式控制
|
||||
- 修复 MCP 按钮消失
|
||||
- 修复创建助手导致空白屏幕
|
||||
- 修复某些情况下上下按钮可见性
|
||||
- 修复钩子在输入法输入时阻止保存
|
||||
- 修复 Azure GPT-image-1 和 OpenRouter Gemini-image
|
||||
- 修复 Silicon 推理问题
|
||||
- 修复主题分支不完整复制,采用两阶段 ID 映射
|
||||
- 修复深度研究模型搜索上下文限制
|
||||
- 修复模型能力检查逻辑
|
||||
- 修复 reranker API 错误响应捕获
|
||||
- 修复右键粘贴文件内容到输入栏
|
||||
- 修复 aiCore 中的 minimax-m2 支持
|
||||
- 修复 Azure embedding 问题
|
||||
- 修复 Agent 编辑模态框加载竞态条件
|
||||
- 修复文件路径更新时防抖保存取消问题
|
||||
- 修复 Perplexity 提供商支持和 API 主机格式化
|
||||
- 修复 CherryAI 提供商支持和 API 主机格式化
|
||||
- 修复绘图图像大小选项的 i18n 翻译
|
||||
- 修复 Agent 会话消息的 token 使用量跟踪
|
||||
- 修复完成或错误时的提示流处理
|
||||
- 修复消息 API 初始化问题
|
||||
<!--LANG:END-->
|
||||
|
||||
@@ -147,7 +147,6 @@
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@heroui/react": "^2.8.3",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@langchain/community": "^1.0.0",
|
||||
"@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch",
|
||||
@@ -349,6 +348,7 @@
|
||||
"striptags": "^3.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"tar": "^7.4.3",
|
||||
"tiny-pinyin": "^1.3.2",
|
||||
@@ -374,6 +374,7 @@
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"resolutions": {
|
||||
"@smithy/types": "4.7.1",
|
||||
"@codemirror/language": "6.11.3",
|
||||
"@codemirror/lint": "6.8.5",
|
||||
"@codemirror/view": "6.38.1",
|
||||
|
||||
@@ -470,3 +470,6 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
// resources/scripts should be maintained manually
|
||||
export const HOME_CHERRY_DIR = '.cherrystudio'
|
||||
|
||||
@@ -18,8 +18,10 @@ import { sortedObjectByKeys } from './sort'
|
||||
// ========== SCRIPT CONFIGURATION AREA - MODIFY SETTINGS HERE ==========
|
||||
const SCRIPT_CONFIG = {
|
||||
// 🔧 Concurrency Control Configuration
|
||||
MAX_CONCURRENT_TRANSLATIONS: 5, // Max concurrent requests (Make sure the concurrency level does not exceed your provider's limits.)
|
||||
TRANSLATION_DELAY_MS: 100, // Delay between requests to avoid rate limiting (Recommended: 100-500ms, Range: 0-5000ms)
|
||||
MAX_CONCURRENT_TRANSLATIONS: process.env.TRANSLATION_MAX_CONCURRENT_REQUESTS
|
||||
? parseInt(process.env.TRANSLATION_MAX_CONCURRENT_REQUESTS)
|
||||
: 5, // Max concurrent requests (Make sure the concurrency level does not exceed your provider's limits.)
|
||||
TRANSLATION_DELAY_MS: process.env.TRANSLATION_DELAY_MS ? parseInt(process.env.TRANSLATION_DELAY_MS) : 500, // Delay between requests to avoid rate limiting (Recommended: 100-500ms, Range: 0-5000ms)
|
||||
|
||||
// 🔑 API Configuration
|
||||
API_KEY: process.env.TRANSLATION_API_KEY || '', // API key from environment variable
|
||||
|
||||
@@ -171,7 +171,7 @@ const swaggerOptions: swaggerJSDoc.Options = {
|
||||
}
|
||||
]
|
||||
},
|
||||
apis: ['./src/main/apiServer/routes/*.ts', './src/main/apiServer/app.ts']
|
||||
apis: ['./src/main/apiServer/routes/**/*.ts', './src/main/apiServer/app.ts']
|
||||
}
|
||||
|
||||
export function setupOpenAPIDocumentation(app: Express) {
|
||||
|
||||
@@ -7,16 +7,33 @@ import { app, Menu, shell } from 'electron'
|
||||
|
||||
import { configManager } from './ConfigManager'
|
||||
export class AppMenuService {
|
||||
private languageChangeCallback?: (newLanguage: string) => void
|
||||
|
||||
constructor() {
|
||||
// Subscribe to language change events
|
||||
this.languageChangeCallback = () => {
|
||||
this.setupApplicationMenu()
|
||||
}
|
||||
configManager.subscribe('language', this.languageChangeCallback)
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
// Clean up subscription to prevent memory leaks
|
||||
if (this.languageChangeCallback) {
|
||||
configManager.unsubscribe('language', this.languageChangeCallback)
|
||||
}
|
||||
}
|
||||
|
||||
public setupApplicationMenu(): void {
|
||||
const locale = locales[configManager.getLanguage()]
|
||||
const { common } = locale.translation
|
||||
const { appMenu } = locale.translation
|
||||
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
label: app.name,
|
||||
submenu: [
|
||||
{
|
||||
label: common.about + ' ' + app.name,
|
||||
label: appMenu.about + ' ' + app.name,
|
||||
click: () => {
|
||||
// Emit event to navigate to About page
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
@@ -27,50 +44,78 @@ export class AppMenuService {
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'services' },
|
||||
{ role: 'services', label: appMenu.services },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ role: 'hide', label: `${appMenu.hide} ${app.name}` },
|
||||
{ role: 'hideOthers', label: appMenu.hideOthers },
|
||||
{ role: 'unhide', label: appMenu.unhide },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' }
|
||||
{ role: 'quit', label: `${appMenu.quit} ${app.name}` }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'fileMenu'
|
||||
label: appMenu.file,
|
||||
submenu: [{ role: 'close', label: appMenu.close }]
|
||||
},
|
||||
{
|
||||
role: 'editMenu'
|
||||
label: appMenu.edit,
|
||||
submenu: [
|
||||
{ role: 'undo', label: appMenu.undo },
|
||||
{ role: 'redo', label: appMenu.redo },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut', label: appMenu.cut },
|
||||
{ role: 'copy', label: appMenu.copy },
|
||||
{ role: 'paste', label: appMenu.paste },
|
||||
{ role: 'delete', label: appMenu.delete },
|
||||
{ role: 'selectAll', label: appMenu.selectAll }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'viewMenu'
|
||||
label: appMenu.view,
|
||||
submenu: [
|
||||
{ role: 'reload', label: appMenu.reload },
|
||||
{ role: 'forceReload', label: appMenu.forceReload },
|
||||
{ role: 'toggleDevTools', label: appMenu.toggleDevTools },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom', label: appMenu.resetZoom },
|
||||
{ role: 'zoomIn', label: appMenu.zoomIn },
|
||||
{ role: 'zoomOut', label: appMenu.zoomOut },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen', label: appMenu.toggleFullscreen }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'windowMenu'
|
||||
label: appMenu.window,
|
||||
submenu: [
|
||||
{ role: 'minimize', label: appMenu.minimize },
|
||||
{ role: 'zoom', label: appMenu.zoom },
|
||||
{ type: 'separator' },
|
||||
{ role: 'front', label: appMenu.front }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
label: appMenu.help,
|
||||
submenu: [
|
||||
{
|
||||
label: 'Website',
|
||||
label: appMenu.website,
|
||||
click: () => {
|
||||
shell.openExternal('https://cherry-ai.com')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Documentation',
|
||||
label: appMenu.documentation,
|
||||
click: () => {
|
||||
shell.openExternal('https://cherry-ai.com/docs')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Feedback',
|
||||
label: appMenu.feedback,
|
||||
click: () => {
|
||||
shell.openExternal('https://github.com/CherryHQ/cherry-studio/issues/new/choose')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Releases',
|
||||
label: appMenu.releases,
|
||||
click: () => {
|
||||
shell.openExternal('https://github.com/CherryHQ/cherry-studio/releases')
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getBinaryName } from '@main/utils/process'
|
||||
import type { TerminalConfig, TerminalConfigWithCommand } from '@shared/config/constant'
|
||||
import {
|
||||
codeTools,
|
||||
HOME_CHERRY_DIR,
|
||||
MACOS_TERMINALS,
|
||||
MACOS_TERMINALS_WITH_COMMANDS,
|
||||
terminalApps,
|
||||
@@ -66,7 +67,7 @@ class CodeToolsService {
|
||||
}
|
||||
|
||||
public async getBunPath() {
|
||||
const dir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const dir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
const bunName = await getBinaryName('bun')
|
||||
const bunPath = path.join(dir, bunName)
|
||||
return bunPath
|
||||
@@ -362,7 +363,7 @@ class CodeToolsService {
|
||||
|
||||
private async isPackageInstalled(cliTool: string): Promise<boolean> {
|
||||
const executableName = await this.getCliExecutableName(cliTool)
|
||||
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
|
||||
|
||||
// Ensure bin directory exists
|
||||
@@ -389,7 +390,7 @@ class CodeToolsService {
|
||||
logger.info(`${cliTool} is installed, getting current version`)
|
||||
try {
|
||||
const executableName = await this.getCliExecutableName(cliTool)
|
||||
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
|
||||
|
||||
const { stdout } = await execAsync(`"${executablePath}" --version`, {
|
||||
@@ -500,7 +501,7 @@ class CodeToolsService {
|
||||
try {
|
||||
const packageName = await this.getPackageName(cliTool)
|
||||
const bunPath = await this.getBunPath()
|
||||
const bunInstallPath = path.join(os.homedir(), '.cherrystudio')
|
||||
const bunInstallPath = path.join(os.homedir(), HOME_CHERRY_DIR)
|
||||
const registryUrl = await this.getNpmRegistryUrl()
|
||||
|
||||
const installEnvPrefix = isWin
|
||||
@@ -550,7 +551,7 @@ class CodeToolsService {
|
||||
const packageName = await this.getPackageName(cliTool)
|
||||
const bunPath = await this.getBunPath()
|
||||
const executableName = await this.getCliExecutableName(cliTool)
|
||||
const binDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const binDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
const executablePath = path.join(binDir, executableName + (isWin ? '.exe' : ''))
|
||||
|
||||
logger.debug(`Package name: ${packageName}`)
|
||||
@@ -652,7 +653,7 @@ class CodeToolsService {
|
||||
baseCommand = `${baseCommand} ${configParams}`
|
||||
}
|
||||
|
||||
const bunInstallPath = path.join(os.homedir(), '.cherrystudio')
|
||||
const bunInstallPath = path.join(os.homedir(), HOME_CHERRY_DIR)
|
||||
|
||||
if (isInstalled) {
|
||||
// If already installed, run executable directly (with optional update message)
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
ToolListChangedNotificationSchema
|
||||
} from '@modelcontextprotocol/sdk/types.js'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
||||
import type { MCPProgressEvent } from '@shared/config/types'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { defaultAppHeaders } from '@shared/utils'
|
||||
@@ -715,7 +716,7 @@ class McpService {
|
||||
}
|
||||
|
||||
public async getInstallInfo() {
|
||||
const dir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const dir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
const uvName = await getBinaryName('uv')
|
||||
const bunName = await getBinaryName('bun')
|
||||
const uvPath = path.join(dir, uvName)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { homedir } from 'node:os'
|
||||
import { promisify } from 'node:util'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
|
||||
@@ -145,7 +146,7 @@ class OvmsManager {
|
||||
*/
|
||||
public async runOvms(): Promise<{ success: boolean; message?: string }> {
|
||||
const homeDir = homedir()
|
||||
const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms')
|
||||
const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
|
||||
const configPath = path.join(ovmsDir, 'models', 'config.json')
|
||||
const runBatPath = path.join(ovmsDir, 'run.bat')
|
||||
|
||||
@@ -195,7 +196,7 @@ class OvmsManager {
|
||||
*/
|
||||
public async getOvmsStatus(): Promise<'not-installed' | 'not-running' | 'running'> {
|
||||
const homeDir = homedir()
|
||||
const ovmsPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'ovms.exe')
|
||||
const ovmsPath = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms', 'ovms.exe')
|
||||
|
||||
try {
|
||||
// Check if OVMS executable exists
|
||||
@@ -273,7 +274,7 @@ class OvmsManager {
|
||||
}
|
||||
|
||||
const homeDir = homedir()
|
||||
const configPath = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms', 'models', 'config.json')
|
||||
const configPath = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms', 'models', 'config.json')
|
||||
try {
|
||||
if (!(await fs.pathExists(configPath))) {
|
||||
logger.warn(`Config file does not exist: ${configPath}`)
|
||||
@@ -304,7 +305,7 @@ class OvmsManager {
|
||||
|
||||
private async applyModelPath(modelDirPath: string): Promise<boolean> {
|
||||
const homeDir = homedir()
|
||||
const patchDir = path.join(homeDir, '.cherrystudio', 'ovms', 'patch')
|
||||
const patchDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'patch')
|
||||
if (!(await fs.pathExists(patchDir))) {
|
||||
return true
|
||||
}
|
||||
@@ -355,7 +356,7 @@ class OvmsManager {
|
||||
logger.info(`Adding model: ${modelName} with ID: ${modelId}, Source: ${modelSource}, Task: ${task}`)
|
||||
|
||||
const homeDir = homedir()
|
||||
const ovdndDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms')
|
||||
const ovdndDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
|
||||
const pathModel = path.join(ovdndDir, 'models', modelId)
|
||||
|
||||
try {
|
||||
@@ -468,7 +469,7 @@ class OvmsManager {
|
||||
*/
|
||||
public async checkModelExists(modelId: string): Promise<boolean> {
|
||||
const homeDir = homedir()
|
||||
const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms')
|
||||
const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
|
||||
const configPath = path.join(ovmsDir, 'models', 'config.json')
|
||||
|
||||
try {
|
||||
@@ -495,7 +496,7 @@ class OvmsManager {
|
||||
*/
|
||||
public async updateModelConfig(modelName: string, modelId: string): Promise<boolean> {
|
||||
const homeDir = homedir()
|
||||
const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms')
|
||||
const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
|
||||
const configPath = path.join(ovmsDir, 'models', 'config.json')
|
||||
|
||||
try {
|
||||
@@ -548,7 +549,7 @@ class OvmsManager {
|
||||
*/
|
||||
public async getModels(): Promise<ModelConfig[]> {
|
||||
const homeDir = homedir()
|
||||
const ovmsDir = path.join(homeDir, '.cherrystudio', 'ovms', 'ovms')
|
||||
const ovmsDir = path.join(homeDir, HOME_CHERRY_DIR, 'ovms', 'ovms')
|
||||
const configPath = path.join(ovmsDir, 'models', 'config.json')
|
||||
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Attributes, SpanEntity, TokenUsage, TraceCache } from '@mcp-trace/
|
||||
import { convertSpanToSpanEntity } from '@mcp-trace/trace-core'
|
||||
import { SpanStatusCode } from '@opentelemetry/api'
|
||||
import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'
|
||||
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
||||
import fs from 'fs/promises'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
@@ -18,7 +19,7 @@ class SpanCacheService implements TraceCache {
|
||||
pri
|
||||
|
||||
constructor() {
|
||||
this.fileDir = path.join(os.homedir(), '.cherrystudio', 'trace')
|
||||
this.fileDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'trace')
|
||||
}
|
||||
|
||||
createSpan: (span: ReadableSpan) => void = (span: ReadableSpan) => {
|
||||
|
||||
@@ -365,6 +365,16 @@ class ClaudeCodeService implements AgentServiceInterface {
|
||||
type: 'chunk',
|
||||
chunk
|
||||
})
|
||||
|
||||
// Close prompt stream when SDK signals completion or error
|
||||
if (chunk.type === 'finish' || chunk.type === 'error') {
|
||||
logger.info('Closing prompt stream as SDK signaled completion', {
|
||||
chunkType: chunk.type,
|
||||
reason: chunk.type === 'finish' ? 'finished' : 'error_occurred'
|
||||
})
|
||||
closePromptStream()
|
||||
logger.info('Prompt stream closed successfully')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { isWin } from '@main/constant'
|
||||
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
||||
import type { OcrOvConfig, OcrResult, SupportedOcrFile } from '@types'
|
||||
import { isImageFileMetadata } from '@types'
|
||||
import { exec } from 'child_process'
|
||||
@@ -13,7 +14,7 @@ import { OcrBaseService } from './OcrBaseService'
|
||||
const logger = loggerService.withContext('OvOcrService')
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
const PATH_BAT_FILE = path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr', 'run.npu.bat')
|
||||
const PATH_BAT_FILE = path.join(os.homedir(), HOME_CHERRY_DIR, 'ovms', 'ovocr', 'run.npu.bat')
|
||||
|
||||
export class OvOcrService extends OcrBaseService {
|
||||
constructor() {
|
||||
@@ -30,7 +31,7 @@ export class OvOcrService extends OcrBaseService {
|
||||
}
|
||||
|
||||
private getOvOcrPath(): string {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'ovms', 'ovocr')
|
||||
return path.join(os.homedir(), HOME_CHERRY_DIR, 'ovms', 'ovocr')
|
||||
}
|
||||
|
||||
private getImgDir(): string {
|
||||
|
||||
@@ -5,7 +5,7 @@ import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { loggerService } from '@logger'
|
||||
import { audioExts, documentExts, imageExts, MB, textExts, videoExts } from '@shared/config/constant'
|
||||
import { audioExts, documentExts, HOME_CHERRY_DIR, imageExts, MB, textExts, videoExts } from '@shared/config/constant'
|
||||
import type { FileMetadata, NotesTreeNode } from '@types'
|
||||
import { FileTypes } from '@types'
|
||||
import chardet from 'chardet'
|
||||
@@ -160,7 +160,7 @@ export function getNotesDir() {
|
||||
}
|
||||
|
||||
export function getConfigDir() {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'config')
|
||||
return path.join(os.homedir(), HOME_CHERRY_DIR, 'config')
|
||||
}
|
||||
|
||||
export function getCacheDir() {
|
||||
@@ -172,7 +172,7 @@ export function getAppConfigDir(name: string) {
|
||||
}
|
||||
|
||||
export function getMcpDir() {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'mcp')
|
||||
return path.join(os.homedir(), HOME_CHERRY_DIR, 'mcp')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,7 @@ import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { isLinux, isPortable, isWin } from '@main/constant'
|
||||
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
||||
import { app } from 'electron'
|
||||
|
||||
// Please don't import any other modules which is not node/electron built-in modules
|
||||
@@ -17,7 +18,7 @@ function hasWritePermission(path: string) {
|
||||
}
|
||||
|
||||
function getConfigDir() {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'config')
|
||||
return path.join(os.homedir(), HOME_CHERRY_DIR, 'config')
|
||||
}
|
||||
|
||||
export function initAppDataDir() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { HOME_CHERRY_DIR } from '@shared/config/constant'
|
||||
import { spawn } from 'child_process'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
@@ -46,11 +47,11 @@ export async function getBinaryName(name: string): Promise<string> {
|
||||
|
||||
export async function getBinaryPath(name?: string): Promise<string> {
|
||||
if (!name) {
|
||||
return path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
return path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
}
|
||||
|
||||
const binaryName = await getBinaryName(name)
|
||||
const binariesDir = path.join(os.homedir(), '.cherrystudio', 'bin')
|
||||
const binariesDir = path.join(os.homedir(), HOME_CHERRY_DIR, 'bin')
|
||||
const binariesDirExists = fs.existsSync(binariesDir)
|
||||
return binariesDirExists ? path.join(binariesDir, binaryName) : binaryName
|
||||
}
|
||||
|
||||
@@ -6,11 +6,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Provider } from 'react-redux'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
import { ToastPortal } from './components/ToastPortal'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AntdProvider from './context/AntdProvider'
|
||||
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
||||
import { HeroUIProvider } from './context/HeroUIProvider'
|
||||
import { NotificationProvider } from './context/NotificationProvider'
|
||||
import StyleSheetManager from './context/StyleSheetManager'
|
||||
import { ThemeProvider } from './context/ThemeProvider'
|
||||
@@ -34,24 +32,21 @@ function App(): React.ReactElement {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HeroUIProvider>
|
||||
<StyleSheetManager>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<NotificationProvider>
|
||||
<CodeStyleProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<Router />
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</CodeStyleProvider>
|
||||
</NotificationProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</StyleSheetManager>
|
||||
<ToastPortal />
|
||||
</HeroUIProvider>
|
||||
<StyleSheetManager>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<NotificationProvider>
|
||||
<CodeStyleProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<Router />
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</CodeStyleProvider>
|
||||
</NotificationProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</StyleSheetManager>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
@@ -39,6 +39,7 @@ vi.mock('@renderer/config/providers', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
isCherryAIProvider: vi.fn(),
|
||||
isPerplexityProvider: vi.fn(),
|
||||
isAnthropicProvider: vi.fn(() => false),
|
||||
isAzureOpenAIProvider: vi.fn(() => false),
|
||||
isGeminiProvider: vi.fn(() => false),
|
||||
@@ -52,7 +53,7 @@ vi.mock('@renderer/hooks/useVertexAI', () => ({
|
||||
createVertexProvider: vi.fn()
|
||||
}))
|
||||
|
||||
import { isCherryAIProvider } from '@renderer/config/providers'
|
||||
import { isCherryAIProvider, isPerplexityProvider } from '@renderer/config/providers'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import type { Model, Provider } from '@renderer/types'
|
||||
import { formatApiHost } from '@renderer/utils/api'
|
||||
@@ -97,6 +98,16 @@ const createCherryAIProvider = (): Provider => ({
|
||||
isSystem: false
|
||||
})
|
||||
|
||||
const createPerplexityProvider = (): Provider => ({
|
||||
id: 'perplexity',
|
||||
type: 'openai',
|
||||
name: 'Perplexity',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://api.perplexity.ai',
|
||||
models: [],
|
||||
isSystem: false
|
||||
})
|
||||
|
||||
describe('Copilot responses routing', () => {
|
||||
beforeEach(() => {
|
||||
;(globalThis as any).window = {
|
||||
@@ -195,3 +206,70 @@ describe('CherryAI provider configuration', () => {
|
||||
expect(actualProvider.apiHost).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Perplexity provider configuration', () => {
|
||||
beforeEach(() => {
|
||||
;(globalThis as any).window = {
|
||||
...(globalThis as any).window,
|
||||
keyv: createWindowKeyv()
|
||||
}
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('formats Perplexity provider apiHost with false parameter', () => {
|
||||
const provider = createPerplexityProvider()
|
||||
const model = createModel('sonar', 'Sonar', 'perplexity')
|
||||
|
||||
// Mock the functions to simulate Perplexity provider detection
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(false)
|
||||
vi.mocked(isPerplexityProvider).mockReturnValue(true)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
// Call getActualProvider which should trigger formatProviderApiHost
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
// Verify that formatApiHost was called with false as the second parameter
|
||||
expect(formatApiHost).toHaveBeenCalledWith('https://api.perplexity.ai', false)
|
||||
expect(actualProvider.apiHost).toBe('https://api.perplexity.ai')
|
||||
})
|
||||
|
||||
it('does not format non-Perplexity provider with false parameter', () => {
|
||||
const provider = {
|
||||
id: 'openai',
|
||||
type: 'openai',
|
||||
name: 'OpenAI',
|
||||
apiKey: 'test-key',
|
||||
apiHost: 'https://api.openai.com',
|
||||
models: [],
|
||||
isSystem: false
|
||||
} as Provider
|
||||
const model = createModel('gpt-4', 'GPT-4', 'openai')
|
||||
|
||||
// Mock the functions to simulate non-Perplexity provider
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(false)
|
||||
vi.mocked(isPerplexityProvider).mockReturnValue(false)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
// Call getActualProvider
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
// Verify that formatApiHost was called with default parameters (true)
|
||||
expect(formatApiHost).toHaveBeenCalledWith('https://api.openai.com')
|
||||
expect(actualProvider.apiHost).toBe('https://api.openai.com/v1')
|
||||
})
|
||||
|
||||
it('handles Perplexity provider with empty apiHost', () => {
|
||||
const provider = createPerplexityProvider()
|
||||
provider.apiHost = ''
|
||||
const model = createModel('sonar', 'Sonar', 'perplexity')
|
||||
|
||||
vi.mocked(isCherryAIProvider).mockReturnValue(false)
|
||||
vi.mocked(isPerplexityProvider).mockReturnValue(true)
|
||||
vi.mocked(getProviderByModel).mockReturnValue(provider)
|
||||
|
||||
const actualProvider = getActualProvider(model)
|
||||
|
||||
expect(formatApiHost).toHaveBeenCalledWith('', false)
|
||||
expect(actualProvider.apiHost).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
isAzureOpenAIProvider,
|
||||
isCherryAIProvider,
|
||||
isGeminiProvider,
|
||||
isNewApiProvider
|
||||
isNewApiProvider,
|
||||
isPerplexityProvider
|
||||
} from '@renderer/config/providers'
|
||||
import {
|
||||
getAwsBedrockAccessKeyId,
|
||||
@@ -103,6 +104,8 @@ function formatProviderApiHost(provider: Provider): Provider {
|
||||
formatted.apiHost = formatVertexApiHost(formatted)
|
||||
} else if (isCherryAIProvider(formatted)) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, false)
|
||||
} else if (isPerplexityProvider(formatted)) {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost, false)
|
||||
} else {
|
||||
formatted.apiHost = formatApiHost(formatted.apiHost)
|
||||
}
|
||||
|
||||
BIN
src/renderer/src/assets/images/providers/mcprouter.webp
Normal file
BIN
src/renderer/src/assets/images/providers/mcprouter.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
@@ -41,11 +41,11 @@ body,
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* #root {
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
} */
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
@import 'tailwindcss' source('../../../../renderer');
|
||||
@import 'tw-animate-css';
|
||||
|
||||
/* heroui */
|
||||
@plugin '../../hero.ts';
|
||||
@source '../../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* 如需自定义:
|
||||
@@ -156,11 +152,6 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
/* To disable drag title bar on toast. tailwind css doesn't provide such class name. */
|
||||
.hero-toast {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Avatar, cn } from '@heroui/react'
|
||||
import { getModelLogoById } from '@renderer/config/models'
|
||||
import type { ApiModel } from '@renderer/types'
|
||||
import React from 'react'
|
||||
|
||||
import Ellipsis from './Ellipsis'
|
||||
|
||||
export interface ModelLabelProps extends Omit<React.ComponentPropsWithRef<'div'>, 'children'> {
|
||||
model?: ApiModel
|
||||
classNames?: {
|
||||
container?: string
|
||||
avatar?: string
|
||||
modelName?: string
|
||||
divider?: string
|
||||
providerName?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const ApiModelLabel: React.FC<ModelLabelProps> = ({ model, className, classNames, ...props }) => {
|
||||
return (
|
||||
<div className={cn('flex items-center gap-1', className, classNames?.container)} {...props}>
|
||||
<Avatar
|
||||
src={model ? (getModelLogoById(model.id) ?? getModelLogoById(model.name)) : undefined}
|
||||
className={cn('h-4 w-4', classNames?.avatar)}
|
||||
/>
|
||||
<Ellipsis className={classNames?.modelName}>{model?.name}</Ellipsis>
|
||||
<span className={classNames?.divider}> | </span>
|
||||
<Ellipsis className={classNames?.providerName}>{model?.provider_name}</Ellipsis>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger } from '@heroui/react'
|
||||
import { Button, Popover } from 'antd'
|
||||
import React from 'react'
|
||||
|
||||
import EmojiPicker from '../EmojiPicker'
|
||||
@@ -10,13 +10,10 @@ type Props = {
|
||||
|
||||
export const EmojiAvatarWithPicker: React.FC<Props> = ({ emoji, onPick }) => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button size="sm" startContent={<span className="text-lg">{emoji}</span>} isIconOnly />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<EmojiPicker onEmojiClick={onPick}></EmojiPicker>
|
||||
</PopoverContent>
|
||||
<Popover content={<EmojiPicker onEmojiClick={onPick} />} trigger="click">
|
||||
<Button type="text" style={{ width: 32, height: 32, fontSize: 18 }}>
|
||||
{emoji}
|
||||
</Button>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { cn } from '@renderer/utils'
|
||||
import type { ButtonProps } from 'antd'
|
||||
import { Button } from 'antd'
|
||||
import React, { memo } from 'react'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@heroui/react'
|
||||
import { CheckIcon, XIcon } from 'lucide-react'
|
||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons'
|
||||
import { Button } from 'antd'
|
||||
import type { FC } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
@@ -28,12 +28,22 @@ const ConfirmDialog: FC<Props> = ({ x, y, message, onConfirm, onCancel }) => {
|
||||
<div className="flex min-w-[160px] items-center rounded-lg border border-[var(--color-border)] bg-[var(--color-background)] p-3 shadow-[0_4px_12px_rgba(0,0,0,0.15)]">
|
||||
<div className="mr-2 text-sm leading-[1.4]">{message}</div>
|
||||
<div className="flex justify-center gap-2">
|
||||
<Button onPress={onCancel} radius="full" className="h-6 w-6 min-w-0 p-1" color="danger">
|
||||
<XIcon className="text-danger-foreground" size={16} />
|
||||
</Button>
|
||||
<Button onPress={onConfirm} radius="full" className="h-6 w-6 min-w-0 p-1" color="success">
|
||||
<CheckIcon className="text-success-foreground" size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
shape="circle"
|
||||
size="small"
|
||||
danger
|
||||
icon={<CloseOutlined />}
|
||||
style={{ width: 24, height: 24, minWidth: 24 }}
|
||||
/>
|
||||
<Button
|
||||
onClick={onConfirm}
|
||||
shape="circle"
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<CheckOutlined />}
|
||||
style={{ width: 24, height: 24, minWidth: 24, backgroundColor: '#52c41a' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { Button } from 'antd'
|
||||
import { Alert, Space } from 'antd'
|
||||
import type { ComponentType, ReactNode } from 'react'
|
||||
import type { FallbackProps } from 'react-error-boundary'
|
||||
@@ -24,10 +24,10 @@ const DefaultFallback: ComponentType<FallbackProps> = (props: FallbackProps): Re
|
||||
type="error"
|
||||
action={
|
||||
<Space>
|
||||
<Button size="sm" onPress={debug}>
|
||||
<Button size="small" onClick={debug}>
|
||||
{t('error.boundary.default.devtools')}
|
||||
</Button>
|
||||
<Button size="sm" onPress={reload}>
|
||||
<Button size="small" onClick={reload}>
|
||||
{t('error.boundary.default.reload')}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { Modal } from 'antd'
|
||||
import { Bot, MessageSquare } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
@@ -51,7 +51,7 @@ const PopupContainer: React.FC<Props> = ({ onSelect, resolve }) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect('assistant')}
|
||||
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
className="group flex cursor-pointer flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
onMouseEnter={() => setHoveredOption('assistant')}
|
||||
onMouseLeave={() => setHoveredOption(null)}>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
|
||||
@@ -73,7 +73,7 @@ const PopupContainer: React.FC<Props> = ({ onSelect, resolve }) => {
|
||||
<button
|
||||
onClick={() => handleSelect('agent')}
|
||||
type="button"
|
||||
className="group flex flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
className="group flex cursor-pointer flex-col items-center gap-3 rounded-lg bg-[var(--color-background-soft)] p-6 transition-all hover:bg-[var(--color-hover)]"
|
||||
onMouseEnter={() => setHoveredOption('agent')}
|
||||
onMouseLeave={() => setHoveredOption(null)}>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-list-item)] transition-colors">
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/modal'
|
||||
import { Progress } from '@heroui/progress'
|
||||
import { Spinner } from '@heroui/spinner'
|
||||
import { loggerService } from '@logger'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { SettingHelpText, SettingRow } from '@renderer/pages/settings'
|
||||
import type { WebSocketCandidatesResponse } from '@shared/config/types'
|
||||
import { Alert, Button, Modal, Progress, Spin } from 'antd'
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -25,7 +22,7 @@ const LoadingQRCode: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
|
||||
<Spinner />
|
||||
<Spin />
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text-2)' }}>
|
||||
{t('settings.data.export_to_phone.lan.generating_qr')}
|
||||
</span>
|
||||
@@ -44,8 +41,8 @@ const ScanQRCode: React.FC<{ qrCodeValue: string }> = ({ qrCodeValue }) => {
|
||||
size={200}
|
||||
imageSettings={{
|
||||
src: AppLogo,
|
||||
width: 60,
|
||||
height: 60,
|
||||
width: 40,
|
||||
height: 40,
|
||||
excavate: true
|
||||
}}
|
||||
/>
|
||||
@@ -72,7 +69,7 @@ const ConnectingAnimation: React.FC = () => {
|
||||
borderRadius: '12px',
|
||||
backgroundColor: 'var(--color-status-warning)'
|
||||
}}>
|
||||
<Spinner size="lg" color="warning" />
|
||||
<Spin size="large" />
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', marginTop: '12px' }}>
|
||||
{t('settings.data.export_to_phone.lan.status.connecting')}
|
||||
</span>
|
||||
@@ -137,7 +134,6 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const [selectedFolderPath, setSelectedFolderPath] = useState<string | null>(null)
|
||||
const [sendProgress, setSendProgress] = useState(0)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [showCloseConfirm, setShowCloseConfirm] = useState(false)
|
||||
const [autoCloseCountdown, setAutoCloseCountdown] = useState<number | null>(null)
|
||||
|
||||
const { t } = useTranslation()
|
||||
@@ -299,22 +295,20 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
// 尝试关闭弹窗 - 如果正在传输则显示确认
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isSending) {
|
||||
setShowCloseConfirm(true)
|
||||
window.modal.confirm({
|
||||
title: t('settings.data.export_to_phone.lan.confirm_close_title'),
|
||||
content: t('settings.data.export_to_phone.lan.confirm_close_message'),
|
||||
centered: true,
|
||||
okButtonProps: {
|
||||
danger: true
|
||||
},
|
||||
okText: t('settings.data.export_to_phone.lan.force_close'),
|
||||
onOk: () => setIsOpen(false)
|
||||
})
|
||||
} else {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}, [isSending])
|
||||
|
||||
// 确认强制关闭
|
||||
const handleForceClose = useCallback(() => {
|
||||
logger.info('Force closing popup during transfer')
|
||||
setIsOpen(false)
|
||||
}, [])
|
||||
|
||||
// 取消关闭确认
|
||||
const handleCancelClose = useCallback(() => {
|
||||
setShowCloseConfirm(false)
|
||||
}, [])
|
||||
}, [isSending, t])
|
||||
|
||||
// 清理并关闭
|
||||
const handleClose = useCallback(async () => {
|
||||
@@ -376,11 +370,13 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
padding: '5px 12px',
|
||||
width: '100%',
|
||||
backgroundColor: connectionStatusStyles.bg,
|
||||
border: `1px solid ${connectionStatusStyles.border}`
|
||||
border: `1px solid ${connectionStatusStyles.border}`,
|
||||
marginBottom: 10
|
||||
}}>
|
||||
<span style={{ fontSize: '14px', fontWeight: '500', color: 'var(--color-text)' }}>{connectionStatusText}</span>
|
||||
</div>
|
||||
@@ -412,7 +408,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
if (!isSending && transferPhase !== 'completed') return null
|
||||
|
||||
return (
|
||||
<div style={{ paddingTop: '8px' }}>
|
||||
<div style={{ paddingTop: '20px' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -441,11 +437,9 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
value={Math.round(sendProgress)}
|
||||
size="md"
|
||||
color={transferPhase === 'completed' ? 'success' : 'primary'}
|
||||
showValueLabel={false}
|
||||
aria-label="Send progress"
|
||||
percent={Math.round(sendProgress)}
|
||||
status={transferPhase === 'completed' ? 'success' : 'active'}
|
||||
showInfo={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -488,95 +482,50 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
handleCancel()
|
||||
}
|
||||
}}
|
||||
isDismissable={false}
|
||||
isKeyboardDismissDisabled={false}
|
||||
placement="center"
|
||||
onClose={handleClose}>
|
||||
<ModalContent>
|
||||
{() => (
|
||||
<>
|
||||
<ModalHeader>{t('settings.data.export_to_phone.lan.title')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<SettingRow>
|
||||
<StatusIndicator />
|
||||
</SettingRow>
|
||||
open={isOpen}
|
||||
onCancel={handleCancel}
|
||||
afterClose={handleClose}
|
||||
title={t('settings.data.export_to_phone.lan.title')}
|
||||
centered
|
||||
closable={!isSending}
|
||||
maskClosable={false}
|
||||
keyboard={true}
|
||||
footer={null}
|
||||
styles={{ body: { paddingBottom: 10 } }}>
|
||||
<SettingRow>
|
||||
<StatusIndicator />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow>
|
||||
<div>{t('settings.data.export_to_phone.lan.content')}</div>
|
||||
</SettingRow>
|
||||
<Alert message={t('settings.data.export_to_phone.lan.content')} type="info" style={{ borderRadius: 0 }} />
|
||||
|
||||
<SettingRow style={{ display: 'flex', justifyContent: 'center', minHeight: '180px' }}>
|
||||
<QRCodeDisplay />
|
||||
</SettingRow>
|
||||
<SettingRow style={{ display: 'flex', justifyContent: 'center', minHeight: '180px', marginBlock: 25 }}>
|
||||
<QRCodeDisplay />
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'center', width: '100%' }}>
|
||||
<Button color="default" variant="flat" onPress={handleSelectZip} isDisabled={isSending}>
|
||||
{t('settings.data.export_to_phone.lan.selectZip')}
|
||||
</Button>
|
||||
<Button color="primary" onPress={handleSendZip} isDisabled={!canSend} isLoading={isSending}>
|
||||
{transferStatusText || t('settings.data.export_to_phone.lan.sendZip')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
<SettingRow style={{ display: 'flex', alignItems: 'center', marginBlock: 10 }}>
|
||||
<div style={{ display: 'flex', gap: 10, justifyContent: 'center', width: '100%' }}>
|
||||
<Button onClick={handleSelectZip} disabled={isSending}>
|
||||
{t('settings.data.export_to_phone.lan.selectZip')}
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleSendZip} disabled={!canSend} loading={isSending}>
|
||||
{transferStatusText || t('settings.data.export_to_phone.lan.sendZip')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingRow>
|
||||
|
||||
<SettingHelpText
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
{selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')}
|
||||
</SettingHelpText>
|
||||
|
||||
<TransferProgress />
|
||||
<AutoCloseCountdown />
|
||||
<ErrorDisplay />
|
||||
</ModalBody>
|
||||
|
||||
{showCloseConfirm && (
|
||||
<ModalFooter>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: '12px',
|
||||
padding: '8px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--color-status-warning)',
|
||||
border: '1px solid var(--color-status-warning)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '20px' }}>⚠️</span>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text)', fontWeight: '500' }}>
|
||||
{t('settings.data.export_to_phone.lan.confirm_close_title')}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: '13px', color: 'var(--color-text-2)', marginLeft: '28px' }}>
|
||||
{t('settings.data.export_to_phone.lan.confirm_close_message')}
|
||||
</span>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '4px' }}>
|
||||
<Button size="sm" color="default" variant="flat" onPress={handleCancelClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" color="danger" onPress={handleForceClose}>
|
||||
{t('settings.data.export_to_phone.lan.force_close')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
<TransferProgress />
|
||||
<AutoCloseCountdown />
|
||||
<ErrorDisplay />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
205
src/renderer/src/components/Popups/UpdateDialogPopup.tsx
Normal file
205
src/renderer/src/components/Popups/UpdateDialogPopup.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { handleSaveData } from '@renderer/store'
|
||||
import { Button, Modal } from 'antd'
|
||||
import type { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Markdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const logger = loggerService.withContext('UpdateDialog')
|
||||
|
||||
interface ShowParams {
|
||||
releaseInfo: UpdateInfo | null
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ releaseInfo, resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(true)
|
||||
const [isInstalling, setIsInstalling] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (releaseInfo) {
|
||||
logger.info('Update dialog opened', { version: releaseInfo.version })
|
||||
}
|
||||
}, [releaseInfo])
|
||||
|
||||
const handleInstall = async () => {
|
||||
setIsInstalling(true)
|
||||
try {
|
||||
await handleSaveData()
|
||||
await window.api.quitAndInstall()
|
||||
setOpen(false)
|
||||
} catch (error) {
|
||||
logger.error('Failed to save data before update', error as Error)
|
||||
setIsInstalling(false)
|
||||
window.toast.error(t('update.saveDataError'))
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
UpdateDialogPopup.hide = onCancel
|
||||
|
||||
const releaseNotes = releaseInfo?.releaseNotes
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<ModalHeaderWrapper>
|
||||
<h3>{t('update.title')}</h3>
|
||||
<p>{t('update.message').replace('{{version}}', releaseInfo?.version || '')}</p>
|
||||
</ModalHeaderWrapper>
|
||||
}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="animation-move-down"
|
||||
centered
|
||||
width={720}
|
||||
footer={[
|
||||
<Button key="later" onClick={onCancel} disabled={isInstalling}>
|
||||
{t('update.later')}
|
||||
</Button>,
|
||||
<Button key="install" type="primary" onClick={handleInstall} loading={isInstalling}>
|
||||
{t('update.install')}
|
||||
</Button>
|
||||
]}>
|
||||
<ModalBodyWrapper>
|
||||
<ReleaseNotesWrapper className="markdown">
|
||||
<Markdown>
|
||||
{typeof releaseNotes === 'string'
|
||||
? releaseNotes
|
||||
: Array.isArray(releaseNotes)
|
||||
? releaseNotes
|
||||
.map((note: ReleaseNoteInfo) => note.note)
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
: t('update.noReleaseNotes')}
|
||||
</Markdown>
|
||||
</ReleaseNotesWrapper>
|
||||
</ModalBodyWrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'UpdateDialogPopup'
|
||||
|
||||
export default class UpdateDialogPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ModalHeaderWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
`
|
||||
|
||||
const ModalBodyWrapper = styled.div`
|
||||
max-height: 450px;
|
||||
overflow-y: auto;
|
||||
padding: 12px 0;
|
||||
`
|
||||
|
||||
const ReleaseNotesWrapper = styled.div`
|
||||
background-color: var(--color-bg-2);
|
||||
border-radius: 8px;
|
||||
|
||||
p {
|
||||
margin: 0 0 12px 0;
|
||||
color: var(--color-text-2);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 16px 0 8px 0;
|
||||
color: var(--color-text-1);
|
||||
font-weight: 600;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 6px;
|
||||
background-color: var(--color-bg-3);
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 12px;
|
||||
background-color: var(--color-bg-3);
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -1,44 +1,32 @@
|
||||
import type { SelectedItemProps } from '@heroui/react'
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Select,
|
||||
SelectItem,
|
||||
Textarea,
|
||||
useDisclosure
|
||||
} from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { Selection } from '@react-types/shared'
|
||||
import ClaudeIcon from '@renderer/assets/images/models/claude.png'
|
||||
import { ErrorBoundary } from '@renderer/components/ErrorBoundary'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { permissionModeCards } from '@renderer/config/agent'
|
||||
import { agentModelFilter, getModelLogoById } from '@renderer/config/models'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiModels } from '@renderer/hooks/agents/useModels'
|
||||
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
|
||||
import SelectAgentBaseModelButton from '@renderer/pages/home/components/SelectAgentBaseModelButton'
|
||||
import type {
|
||||
AddAgentForm,
|
||||
AgentEntity,
|
||||
AgentType,
|
||||
ApiModel,
|
||||
BaseAgentForm,
|
||||
PermissionMode,
|
||||
Tool,
|
||||
UpdateAgentForm
|
||||
} from '@renderer/types'
|
||||
import { AgentConfigurationSchema, isAgentType } from '@renderer/types'
|
||||
import { Avatar, Button, Input, Modal, Select } from 'antd'
|
||||
import { AlertTriangleIcon } from 'lucide-react'
|
||||
import type { ChangeEvent, FormEvent } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { ErrorBoundary } from '../../ErrorBoundary'
|
||||
import type { BaseOption, ModelOption } from './shared'
|
||||
import { Option, renderOption } from './shared'
|
||||
import type { BaseOption } from './shared'
|
||||
|
||||
const { TextArea } = Input
|
||||
|
||||
const logger = loggerService.withContext('AddAgentPopup')
|
||||
|
||||
@@ -48,8 +36,6 @@ interface AgentTypeOption extends BaseOption {
|
||||
name: AgentEntity['name']
|
||||
}
|
||||
|
||||
type Option = AgentTypeOption | ModelOption
|
||||
|
||||
type AgentWithTools = AgentEntity & { tools?: Tool[] }
|
||||
|
||||
const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
|
||||
@@ -64,58 +50,37 @@ const buildAgentForm = (existing?: AgentWithTools): BaseAgentForm => ({
|
||||
configuration: AgentConfigurationSchema.parse(existing?.configuration ?? {})
|
||||
})
|
||||
|
||||
type Props = {
|
||||
interface ShowParams {
|
||||
agent?: AgentWithTools
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
afterSubmit?: (a: AgentEntity) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal component for creating or editing an agent.
|
||||
*
|
||||
* Either trigger or isOpen and onClose is given.
|
||||
* @param agent - Optional agent entity for editing mode.
|
||||
* @param isOpen - Optional controlled modal open state. From useDisclosure.
|
||||
* @param onClose - Optional callback when modal closes. From useDisclosure.
|
||||
* @returns Modal component for agent creation/editing
|
||||
*/
|
||||
export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _onClose, afterSubmit }) => {
|
||||
const { isOpen, onClose } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ agent, afterSubmit, resolve }) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(true)
|
||||
const loadingRef = useRef(false)
|
||||
// const { setTimeoutTimer } = useTimer()
|
||||
const { addAgent } = useAgents()
|
||||
const { updateAgent } = useUpdateAgent()
|
||||
// hard-coded. We only support anthropic for now.
|
||||
const { models } = useApiModels({ providerType: 'anthropic' })
|
||||
const isEditing = (agent?: AgentWithTools) => agent !== undefined
|
||||
|
||||
const [form, setForm] = useState<BaseAgentForm>(() => buildAgentForm(agent))
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (open) {
|
||||
setForm(buildAgentForm(agent))
|
||||
}
|
||||
}, [agent, isOpen])
|
||||
}, [agent, open])
|
||||
|
||||
const selectedPermissionMode = form.configuration?.permission_mode ?? 'default'
|
||||
|
||||
const onPermissionModeChange = useCallback((keys: Selection) => {
|
||||
if (keys === 'all') {
|
||||
return
|
||||
}
|
||||
|
||||
const [first] = Array.from(keys)
|
||||
if (!first) {
|
||||
return
|
||||
}
|
||||
|
||||
const onPermissionModeChange = useCallback((value: PermissionMode) => {
|
||||
setForm((prev) => {
|
||||
const parsedConfiguration = AgentConfigurationSchema.parse(prev.configuration ?? {})
|
||||
const nextMode = first as PermissionMode
|
||||
|
||||
if (parsedConfiguration.permission_mode === nextMode) {
|
||||
if (parsedConfiguration.permission_mode === value) {
|
||||
if (!prev.configuration) {
|
||||
return {
|
||||
...prev,
|
||||
@@ -129,7 +94,7 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
...prev,
|
||||
configuration: {
|
||||
...parsedConfiguration,
|
||||
permission_mode: nextMode
|
||||
permission_mode: value
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -150,55 +115,57 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
[]
|
||||
)
|
||||
|
||||
const agentOptions: AgentTypeOption[] = useMemo(
|
||||
const agentOptions = useMemo(
|
||||
() =>
|
||||
agentConfig.map(
|
||||
(option) =>
|
||||
({
|
||||
...option,
|
||||
rendered: <Option option={option} />
|
||||
}) as const satisfies SelectedItemProps
|
||||
),
|
||||
agentConfig.map((option) => ({
|
||||
value: option.key,
|
||||
label: (
|
||||
<OptionWrapper>
|
||||
<Avatar src={option.avatar} size={24} />
|
||||
<span>{option.label}</span>
|
||||
</OptionWrapper>
|
||||
)
|
||||
})),
|
||||
[agentConfig]
|
||||
)
|
||||
|
||||
const onAgentTypeChange = useCallback(
|
||||
(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
(value: AgentType) => {
|
||||
const prevConfig = agentConfig.find((config) => config.key === form.type)
|
||||
let newName: string | undefined = form.name
|
||||
if (prevConfig && prevConfig.name === form.name) {
|
||||
const newConfig = agentConfig.find((config) => config.key === e.target.value)
|
||||
const newConfig = agentConfig.find((config) => config.key === value)
|
||||
if (newConfig) {
|
||||
newName = newConfig.name
|
||||
}
|
||||
}
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
type: e.target.value as AgentType,
|
||||
type: value,
|
||||
name: newName
|
||||
}))
|
||||
},
|
||||
[agentConfig, form.name, form.type]
|
||||
)
|
||||
|
||||
const onNameChange = useCallback((name: string) => {
|
||||
const onNameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
name
|
||||
name: e.target.value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onDescChange = useCallback((description: string) => {
|
||||
const onDescChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
description
|
||||
description: e.target.value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onInstChange = useCallback((instructions: string) => {
|
||||
const onInstChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
instructions
|
||||
instructions: e.target.value
|
||||
}))
|
||||
}, [])
|
||||
|
||||
@@ -231,34 +198,36 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const modelOptions = useMemo(() => {
|
||||
// mocked data. not final version
|
||||
return (models ?? [])
|
||||
.filter((m) =>
|
||||
agentModelFilter({
|
||||
id: m.id,
|
||||
provider: m.provider || '',
|
||||
name: m.name,
|
||||
group: ''
|
||||
})
|
||||
)
|
||||
.map((model) => ({
|
||||
type: 'model',
|
||||
key: model.id,
|
||||
label: model.name,
|
||||
avatar: getModelLogoById(model.id),
|
||||
providerId: model.provider,
|
||||
providerName: model.provider_name
|
||||
})) satisfies ModelOption[]
|
||||
}, [models])
|
||||
// Create a temporary agentBase object for SelectAgentBaseModelButton
|
||||
const tempAgentBase: AgentEntity = useMemo(
|
||||
() => ({
|
||||
id: agent?.id ?? 'temp-creating',
|
||||
type: form.type,
|
||||
name: form.name,
|
||||
model: form.model,
|
||||
accessible_paths: form.accessible_paths.length > 0 ? form.accessible_paths : ['/'],
|
||||
allowed_tools: form.allowed_tools ?? [],
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
configuration: form.configuration,
|
||||
created_at: agent?.created_at ?? new Date().toISOString(),
|
||||
updated_at: agent?.updated_at ?? new Date().toISOString()
|
||||
}),
|
||||
[form, agent?.id, agent?.created_at, agent?.updated_at]
|
||||
)
|
||||
|
||||
const onModelChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
model: e.target.value
|
||||
}))
|
||||
const handleModelSelect = useCallback(async (model: ApiModel) => {
|
||||
setForm((prev) => ({ ...prev, model: model.id }))
|
||||
}, [])
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
@@ -330,9 +299,7 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
afterSubmit?.(result.data)
|
||||
}
|
||||
loadingRef.current = false
|
||||
|
||||
// setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
onClose()
|
||||
setOpen(false)
|
||||
},
|
||||
[
|
||||
form.type,
|
||||
@@ -344,7 +311,6 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
form.allowed_tools,
|
||||
form.configuration,
|
||||
agent,
|
||||
onClose,
|
||||
t,
|
||||
updateAgent,
|
||||
afterSubmit,
|
||||
@@ -352,138 +318,312 @@ export const AgentModal: React.FC<Props> = ({ agent, isOpen: _isOpen, onClose: _
|
||||
]
|
||||
)
|
||||
|
||||
AgentModalPopup.hide = onCancel
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
classNames={{
|
||||
base: 'max-h-[90vh]',
|
||||
wrapper: 'overflow-hidden'
|
||||
}}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>{isEditing(agent) ? t('agent.edit.title') : t('agent.add.title')}</ModalHeader>
|
||||
<Form onSubmit={onSubmit} className="min-h-0 w-full shrink overflow-auto">
|
||||
<ModalBody className="min-h-0 w-full flex-1 shrink overflow-auto">
|
||||
<div className="flex gap-2">
|
||||
<Select
|
||||
isRequired
|
||||
isDisabled={isEditing(agent)}
|
||||
selectionMode="single"
|
||||
selectedKeys={[form.type]}
|
||||
disallowEmptySelection
|
||||
onChange={onAgentTypeChange}
|
||||
items={agentOptions}
|
||||
label={t('agent.type.label')}
|
||||
placeholder={t('agent.add.type.placeholder')}
|
||||
renderValue={renderOption}>
|
||||
{(option) => (
|
||||
<SelectItem key={option.key} textValue={option.label}>
|
||||
<Option option={option} />
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<Input isRequired value={form.name} onValueChange={onNameChange} label={t('common.name')} />
|
||||
</div>
|
||||
<Select
|
||||
isRequired
|
||||
selectionMode="single"
|
||||
selectedKeys={form.model ? [form.model] : []}
|
||||
disallowEmptySelection
|
||||
onChange={onModelChange}
|
||||
items={modelOptions}
|
||||
label={t('common.model')}
|
||||
placeholder={t('common.placeholders.select.model')}
|
||||
renderValue={renderOption}>
|
||||
{(option) => (
|
||||
<SelectItem key={option.key} textValue={option.label}>
|
||||
<Option option={option} />
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<Select
|
||||
isRequired
|
||||
selectionMode="single"
|
||||
selectedKeys={[selectedPermissionMode]}
|
||||
onSelectionChange={onPermissionModeChange}
|
||||
label={t('agent.settings.tooling.permissionMode.title', 'Permission mode')}
|
||||
placeholder={t('agent.settings.tooling.permissionMode.placeholder', 'Select permission mode')}
|
||||
description={t(
|
||||
'agent.settings.tooling.permissionMode.helper',
|
||||
'Choose how the agent handles tool approvals.'
|
||||
)}
|
||||
items={permissionModeCards}>
|
||||
{(item) => (
|
||||
<SelectItem key={item.mode} textValue={t(item.titleKey, item.titleFallback)}>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium text-sm">{t(item.titleKey, item.titleFallback)}</span>
|
||||
<span className="text-foreground-500 text-xs">
|
||||
{t(item.descriptionKey, item.descriptionFallback)}
|
||||
</span>
|
||||
<span className="text-foreground-400 text-xs">
|
||||
{t(item.behaviorKey, item.behaviorFallback)}
|
||||
</span>
|
||||
{item.caution ? (
|
||||
<span className="flex items-center gap-1 text-danger-500 text-xs">
|
||||
<AlertTriangleIcon size={12} className="text-danger" />
|
||||
{t(
|
||||
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
|
||||
'Use with caution — all tools will run without asking for approval.'
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
title={isEditing(agent) ? t('agent.edit.title') : t('agent.add.title')}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName="animation-move-down"
|
||||
centered
|
||||
width={500}
|
||||
footer={null}>
|
||||
<StyledForm onSubmit={onSubmit}>
|
||||
<FormContent>
|
||||
<FormRow>
|
||||
<FormItem style={{ flex: 1 }}>
|
||||
<Label>{t('agent.type.label')}</Label>
|
||||
<Select
|
||||
value={form.type}
|
||||
onChange={onAgentTypeChange}
|
||||
options={agentOptions}
|
||||
disabled={isEditing(agent)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem style={{ flex: 1 }}>
|
||||
<Label>
|
||||
{t('common.name')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<Input value={form.name} onChange={onNameChange} required />
|
||||
</FormItem>
|
||||
</FormRow>
|
||||
|
||||
<FormItem>
|
||||
<Label>
|
||||
{t('common.model')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<SelectAgentBaseModelButton
|
||||
agentBase={tempAgentBase}
|
||||
onSelect={handleModelSelect}
|
||||
fontSize={14}
|
||||
avatarSize={24}
|
||||
iconSize={16}
|
||||
buttonStyle={{
|
||||
padding: '8px 12px',
|
||||
width: '100%',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: 6,
|
||||
height: 'auto'
|
||||
}}
|
||||
containerClassName="flex items-center justify-between w-full"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Label>
|
||||
{t('agent.settings.tooling.permissionMode.title', 'Permission mode')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedPermissionMode}
|
||||
onChange={onPermissionModeChange}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('agent.settings.tooling.permissionMode.placeholder', 'Select permission mode')}
|
||||
dropdownStyle={{ minWidth: '500px' }}
|
||||
optionLabelProp="label">
|
||||
{permissionModeCards.map((item) => (
|
||||
<Select.Option key={item.mode} value={item.mode} label={t(item.titleKey, item.titleFallback)}>
|
||||
<PermissionOptionWrapper>
|
||||
<div className="title">{t(item.titleKey, item.titleFallback)}</div>
|
||||
<div className="description">{t(item.descriptionKey, item.descriptionFallback)}</div>
|
||||
<div className="behavior">{t(item.behaviorKey, item.behaviorFallback)}</div>
|
||||
{item.caution && (
|
||||
<div className="caution">
|
||||
<AlertTriangleIcon size={12} />
|
||||
{t(
|
||||
'agent.settings.tooling.permissionMode.bypassPermissions.warning',
|
||||
'Use with caution — all tools will run without asking for approval.'
|
||||
)}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-foreground text-sm">
|
||||
{t('agent.session.accessible_paths.label')}
|
||||
</span>
|
||||
<Button size="sm" variant="flat" onPress={addAccessiblePath}>
|
||||
{t('agent.session.accessible_paths.add')}
|
||||
)}
|
||||
</PermissionOptionWrapper>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<HelpText>
|
||||
{t('agent.settings.tooling.permissionMode.helper', 'Choose how the agent handles tool approvals.')}
|
||||
</HelpText>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<LabelWithButton>
|
||||
<Label>
|
||||
{t('agent.session.accessible_paths.label')} <RequiredMark>*</RequiredMark>
|
||||
</Label>
|
||||
<Button size="small" onClick={addAccessiblePath}>
|
||||
{t('agent.session.accessible_paths.add')}
|
||||
</Button>
|
||||
</LabelWithButton>
|
||||
{form.accessible_paths.length > 0 ? (
|
||||
<PathList>
|
||||
{form.accessible_paths.map((path) => (
|
||||
<PathItem key={path}>
|
||||
<PathText title={path}>{path}</PathText>
|
||||
<Button size="small" danger onClick={() => removeAccessiblePath(path)}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
{form.accessible_paths.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{form.accessible_paths.map((path) => (
|
||||
<div
|
||||
key={path}
|
||||
className="flex items-center justify-between gap-2 rounded-medium border border-default-200 px-3 py-2">
|
||||
<span className="truncate text-sm" title={path}>
|
||||
{path}
|
||||
</span>
|
||||
<Button size="sm" variant="light" color="danger" onPress={() => removeAccessiblePath(path)}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-foreground-400 text-sm">{t('agent.session.accessible_paths.empty')}</p>
|
||||
)}
|
||||
</div>
|
||||
<Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} />
|
||||
<Textarea
|
||||
label={t('common.description')}
|
||||
value={form.description ?? ''}
|
||||
onValueChange={onDescChange}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter className="w-full">
|
||||
<Button onPress={onClose}>{t('common.close')}</Button>
|
||||
<Button color="primary" type="submit" isLoading={loadingRef.current}>
|
||||
{isEditing(agent) ? t('common.confirm') : t('common.add')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</PathItem>
|
||||
))}
|
||||
</PathList>
|
||||
) : (
|
||||
<EmptyText>{t('agent.session.accessible_paths.empty')}</EmptyText>
|
||||
)}
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Label>{t('common.prompt')}</Label>
|
||||
<TextArea rows={3} value={form.instructions ?? ''} onChange={onInstChange} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Label>{t('common.description')}</Label>
|
||||
<TextArea rows={2} value={form.description ?? ''} onChange={onDescChange} />
|
||||
</FormItem>
|
||||
</FormContent>
|
||||
|
||||
<FormFooter>
|
||||
<Button onClick={onCancel}>{t('common.close')}</Button>
|
||||
<Button type="primary" htmlType="submit" loading={loadingRef.current}>
|
||||
{isEditing(agent) ? t('common.confirm') : t('common.add')}
|
||||
</Button>
|
||||
</FormFooter>
|
||||
</StyledForm>
|
||||
</Modal>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'AgentModalPopup'
|
||||
|
||||
export default class AgentModalPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
TopView.hide(TopViewKey)
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the old export for backward compatibility during migration
|
||||
export const AgentModal = AgentModalPopup
|
||||
|
||||
const StyledForm = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const FormContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
`
|
||||
|
||||
const FormRow = styled.div`
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
`
|
||||
|
||||
const FormItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const Label = styled.label`
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const RequiredMark = styled.span`
|
||||
color: #ff4d4f;
|
||||
margin-left: 4px;
|
||||
`
|
||||
|
||||
const HelpText = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const LabelWithButton = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const PathList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const PathItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-bg-1);
|
||||
`
|
||||
|
||||
const PathText = styled.span`
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const EmptyText = styled.p`
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
margin: 0;
|
||||
`
|
||||
|
||||
const FormFooter = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
`
|
||||
|
||||
const OptionWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const PermissionOptionWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px 0;
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.behavior {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.caution {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #ff4d4f;
|
||||
margin-top: 4px;
|
||||
padding: 6px 8px;
|
||||
background-color: rgba(255, 77, 79, 0.1);
|
||||
border-radius: 4px;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Textarea,
|
||||
useDisclosure
|
||||
} from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { Selection } from '@react-types/shared'
|
||||
import { AllowedToolsSelect } from '@renderer/components/agent'
|
||||
import { useAgent } from '@renderer/hooks/agents/useAgent'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import type {
|
||||
AgentEntity,
|
||||
AgentSessionEntity,
|
||||
BaseSessionForm,
|
||||
CreateSessionForm,
|
||||
Tool,
|
||||
UpdateSessionForm
|
||||
} from '@renderer/types'
|
||||
import type { FormEvent, ReactNode } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { ErrorBoundary } from '../../ErrorBoundary'
|
||||
|
||||
const logger = loggerService.withContext('SessionAgentPopup')
|
||||
|
||||
type AgentWithTools = AgentEntity & { tools?: Tool[] }
|
||||
type SessionWithTools = AgentSessionEntity & { tools?: Tool[] }
|
||||
|
||||
const buildSessionForm = (existing?: SessionWithTools, agent?: AgentWithTools): BaseSessionForm => ({
|
||||
name: existing?.name ?? agent?.name ?? 'Claude Code',
|
||||
description: existing?.description ?? agent?.description,
|
||||
instructions: existing?.instructions ?? agent?.instructions,
|
||||
model: existing?.model ?? agent?.model ?? '',
|
||||
accessible_paths: existing?.accessible_paths
|
||||
? [...existing.accessible_paths]
|
||||
: agent?.accessible_paths
|
||||
? [...agent.accessible_paths]
|
||||
: [],
|
||||
allowed_tools: existing?.allowed_tools
|
||||
? [...existing.allowed_tools]
|
||||
: agent?.allowed_tools
|
||||
? [...agent.allowed_tools]
|
||||
: [],
|
||||
mcps: existing?.mcps ? [...existing.mcps] : agent?.mcps ? [...agent.mcps] : []
|
||||
})
|
||||
|
||||
interface BaseProps {
|
||||
agentId: string
|
||||
session?: SessionWithTools
|
||||
onSessionCreated?: (session: AgentSessionEntity) => void
|
||||
}
|
||||
|
||||
interface TriggerProps extends BaseProps {
|
||||
trigger: { content: ReactNode; className?: string }
|
||||
isOpen?: never
|
||||
onClose?: never
|
||||
}
|
||||
|
||||
interface StateProps extends BaseProps {
|
||||
trigger?: never
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type Props = TriggerProps | StateProps
|
||||
|
||||
/**
|
||||
* Modal component for creating or editing a Session.
|
||||
* @deprecated may as a reference when migrating to v2
|
||||
*
|
||||
* Either trigger or isOpen and onClose is given.
|
||||
* @param agentId - The ID of agent which the session is related.
|
||||
* @param session - Optional session entity for editing mode.
|
||||
* @param trigger - Optional trigger element that opens the modal. It MUST propagate the click event to trigger the modal.
|
||||
* @param isOpen - Optional controlled modal open state. From useDisclosure.
|
||||
* @param onClose - Optional callback when modal closes. From useDisclosure.
|
||||
* @returns Modal component for agent creation/editing
|
||||
*/
|
||||
export const SessionModal: React.FC<Props> = ({
|
||||
agentId,
|
||||
session,
|
||||
trigger,
|
||||
isOpen: _isOpen,
|
||||
onClose: _onClose,
|
||||
onSessionCreated
|
||||
}) => {
|
||||
const { isOpen, onClose, onOpen } = useDisclosure({ isOpen: _isOpen, onClose: _onClose })
|
||||
const { t } = useTranslation()
|
||||
const loadingRef = useRef(false)
|
||||
// const { setTimeoutTimer } = useTimer()
|
||||
const { createSession } = useSessions(agentId)
|
||||
const { updateSession } = useUpdateSession(agentId)
|
||||
const { agent } = useAgent(agentId)
|
||||
const isEditing = (session?: AgentSessionEntity) => session !== undefined
|
||||
|
||||
const [form, setForm] = useState<BaseSessionForm>(() => buildSessionForm(session, agent ?? undefined))
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setForm(buildSessionForm(session, agent ?? undefined))
|
||||
}
|
||||
}, [session, agent, isOpen])
|
||||
|
||||
const availableTools = useMemo(() => session?.tools ?? agent?.tools ?? [], [agent?.tools, session?.tools])
|
||||
const selectedToolKeys = useMemo(() => new Set(form.allowed_tools ?? []), [form.allowed_tools])
|
||||
|
||||
useEffect(() => {
|
||||
if (!availableTools.length) {
|
||||
return
|
||||
}
|
||||
|
||||
setForm((prev) => {
|
||||
const allowed = prev.allowed_tools ?? []
|
||||
const validTools = allowed.filter((id) => availableTools.some((tool) => tool.id === id))
|
||||
if (validTools.length === allowed.length) {
|
||||
return prev
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
allowed_tools: validTools
|
||||
}
|
||||
})
|
||||
}, [availableTools])
|
||||
|
||||
const onNameChange = useCallback((name: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
name
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onDescChange = useCallback((description: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
description
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onInstChange = useCallback((instructions: string) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
instructions
|
||||
}))
|
||||
}, [])
|
||||
|
||||
const onAllowedToolsChange = useCallback(
|
||||
(keys: Selection) => {
|
||||
setForm((prev) => {
|
||||
const existing = prev.allowed_tools ?? []
|
||||
if (keys === 'all') {
|
||||
return {
|
||||
...prev,
|
||||
allowed_tools: availableTools.map((tool) => tool.id)
|
||||
}
|
||||
}
|
||||
|
||||
const next = Array.from(keys).map(String)
|
||||
const filtered = availableTools.length
|
||||
? next.filter((id) => availableTools.some((tool) => tool.id === id))
|
||||
: next
|
||||
|
||||
if (existing.length === filtered.length && existing.every((id) => filtered.includes(id))) {
|
||||
return prev
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
allowed_tools: filtered
|
||||
}
|
||||
})
|
||||
},
|
||||
[availableTools]
|
||||
)
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
if (loadingRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
loadingRef.current = true
|
||||
|
||||
// Additional validation check besides native HTML validation to ensure security
|
||||
if (!form.model) {
|
||||
window.toast.error(t('error.model.not_exists'))
|
||||
loadingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
if (form.accessible_paths.length === 0) {
|
||||
window.toast.error(t('agent.session.accessible_paths.error.at_least_one'))
|
||||
loadingRef.current = false
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEditing(session)) {
|
||||
if (!session) {
|
||||
throw new Error('Agent is required for editing mode')
|
||||
}
|
||||
|
||||
const updatePayload = {
|
||||
id: session.id,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
model: form.model,
|
||||
accessible_paths: [...form.accessible_paths],
|
||||
allowed_tools: [...(form.allowed_tools ?? [])],
|
||||
mcps: [...(form.mcps ?? [])]
|
||||
} satisfies UpdateSessionForm
|
||||
|
||||
updateSession(updatePayload)
|
||||
logger.debug('Updated agent', updatePayload)
|
||||
} else {
|
||||
const newSession = {
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
instructions: form.instructions,
|
||||
model: form.model,
|
||||
accessible_paths: [...form.accessible_paths],
|
||||
allowed_tools: [...(form.allowed_tools ?? [])],
|
||||
mcps: [...(form.mcps ?? [])]
|
||||
} satisfies CreateSessionForm
|
||||
const createdSession = await createSession(newSession)
|
||||
if (createdSession) {
|
||||
onSessionCreated?.(createdSession)
|
||||
}
|
||||
logger.debug('Added agent', newSession)
|
||||
}
|
||||
|
||||
// setTimeoutTimer('onCreateAgent', () => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||
onClose()
|
||||
} finally {
|
||||
loadingRef.current = false
|
||||
}
|
||||
},
|
||||
[
|
||||
form.model,
|
||||
form.name,
|
||||
form.description,
|
||||
form.instructions,
|
||||
form.accessible_paths,
|
||||
form.allowed_tools,
|
||||
form.mcps,
|
||||
session,
|
||||
onClose,
|
||||
onSessionCreated,
|
||||
t,
|
||||
updateSession,
|
||||
createSession
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
{/* NOTE: Hero UI Modal Pattern: Combine the Button and Modal components into a single
|
||||
encapsulated component. This is because the Modal component needs to bind the onOpen
|
||||
event handler to the Button for proper focus management.
|
||||
|
||||
Or just use external isOpen/onOpen/onClose to control modal state.
|
||||
*/}
|
||||
|
||||
{trigger && (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onOpen()
|
||||
}}
|
||||
className={cn('w-full', trigger.className)}>
|
||||
{trigger.content}
|
||||
</div>
|
||||
)}
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
<>
|
||||
<ModalHeader>
|
||||
{isEditing(session) ? t('agent.session.edit.title') : t('agent.session.add.title')}
|
||||
</ModalHeader>
|
||||
<Form onSubmit={onSubmit} className="w-full">
|
||||
<ModalBody className="w-full">
|
||||
<Input isRequired value={form.name} onValueChange={onNameChange} label={t('common.name')} />
|
||||
<Textarea
|
||||
label={t('common.description')}
|
||||
value={form.description ?? ''}
|
||||
onValueChange={onDescChange}
|
||||
/>
|
||||
<AllowedToolsSelect
|
||||
items={availableTools}
|
||||
selectedKeys={selectedToolKeys}
|
||||
onSelectionChange={onAllowedToolsChange}
|
||||
/>
|
||||
<Textarea label={t('common.prompt')} value={form.instructions ?? ''} onValueChange={onInstChange} />
|
||||
</ModalBody>
|
||||
<ModalFooter className="w-full">
|
||||
<Button onPress={onClose}>{t('common.close')}</Button>
|
||||
<Button color="primary" type="submit" isLoading={loadingRef.current}>
|
||||
{isEditing(session) ? t('common.confirm') : t('common.add')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,3 @@
|
||||
import type { SelectedItemProps, SelectedItems } from '@heroui/react'
|
||||
import { Avatar } from '@heroui/react'
|
||||
import { getProviderLabel } from '@renderer/i18n/label'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface BaseOption {
|
||||
type: 'type' | 'model'
|
||||
key: string
|
||||
@@ -10,43 +5,3 @@ export interface BaseOption {
|
||||
// img src
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
export interface ModelOption extends BaseOption {
|
||||
providerId?: string
|
||||
providerName?: string
|
||||
}
|
||||
|
||||
export function isModelOption(option: BaseOption): option is ModelOption {
|
||||
return option.type === 'model'
|
||||
}
|
||||
|
||||
export const Item = ({ item }: { item: SelectedItemProps<BaseOption> }) => <Option option={item.data} />
|
||||
|
||||
export const renderOption = (items: SelectedItems<BaseOption>) =>
|
||||
items.map((item) => <Item key={item.key} item={item} />)
|
||||
|
||||
export const Option = ({ option }: { option?: BaseOption | null }) => {
|
||||
const { t } = useTranslation()
|
||||
if (!option) {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Avatar name="?" className="h-5 w-5" />
|
||||
{t('common.invalid_value')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const providerLabel = (() => {
|
||||
if (!isModelOption(option)) return null
|
||||
if (option.providerName) return option.providerName
|
||||
if (option.providerId) return getProviderLabel(option.providerId)
|
||||
return null
|
||||
})()
|
||||
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Avatar src={option.avatar} className="h-5 w-5" />
|
||||
<span className="truncate">{option.label}</span>
|
||||
{providerLabel ? <span className="truncate text-foreground-500">| {providerLabel}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { ToastProvider } from '@heroui/toast'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
export const ToastPortal = () => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
return () => setMounted(false)
|
||||
}, [])
|
||||
|
||||
if (!mounted) return null
|
||||
|
||||
return createPortal(
|
||||
<ToastProvider
|
||||
placement="top-center"
|
||||
regionProps={{
|
||||
className: 'z-[1001]'
|
||||
}}
|
||||
toastOffset={20}
|
||||
toastProps={{
|
||||
timeout: 3000,
|
||||
classNames: {
|
||||
// This setting causes the 'hero-toast' class to be applied twice to the toast element. This is weird and I don't know why, but it works.
|
||||
base: 'hero-toast'
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
@@ -2,12 +2,12 @@
|
||||
import TopViewMinappContainer from '@renderer/components/MinApp/TopViewMinappContainer'
|
||||
import { useAppInit } from '@renderer/hooks/useAppInit'
|
||||
import { useShortcuts } from '@renderer/hooks/useShortcuts'
|
||||
import { Modal } from 'antd'
|
||||
import { message, Modal } from 'antd'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { Box } from '../Layout'
|
||||
import { getToastUtilities } from './toast'
|
||||
import { getToastUtilities, initMessageApi } from './toast'
|
||||
|
||||
let onPop = () => {}
|
||||
let onShow = ({ element, id }: { element: React.FC | React.ReactNode; id: string }) => {
|
||||
@@ -36,6 +36,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
elementsRef.current = elements
|
||||
|
||||
const [modal, modalContextHolder] = Modal.useModal()
|
||||
const [messageApi, messageContextHolder] = message.useMessage()
|
||||
const { shortcuts } = useShortcuts()
|
||||
const enableQuitFullScreen = shortcuts.find((item) => item.key === 'exit_fullscreen')?.enabled
|
||||
|
||||
@@ -43,8 +44,9 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
|
||||
useEffect(() => {
|
||||
window.modal = modal
|
||||
initMessageApi(messageApi)
|
||||
window.toast = getToastUtilities()
|
||||
}, [modal])
|
||||
}, [messageApi, modal])
|
||||
|
||||
onPop = () => {
|
||||
const views = [...elementsRef.current]
|
||||
@@ -97,6 +99,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{messageContextHolder}
|
||||
{modalContextHolder}
|
||||
<TopViewMinappContainer />
|
||||
{elements.map(({ element: Element, id }) => (
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
|
||||
import type { RequireSome } from '@renderer/types'
|
||||
|
||||
type AddToastProps = Parameters<typeof addToast>[0]
|
||||
type ToastPropsColored = Omit<AddToastProps, 'color'>
|
||||
|
||||
const createToast = (color: 'danger' | 'success' | 'warning' | 'default') => {
|
||||
return (arg: ToastPropsColored | string): string | null => {
|
||||
if (typeof arg === 'string') {
|
||||
return addToast({ color, title: arg })
|
||||
} else {
|
||||
return addToast({ color, ...arg })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// syntatic sugar, oh yeah
|
||||
|
||||
/**
|
||||
* Display an error toast notification with red color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const error = createToast('danger')
|
||||
|
||||
/**
|
||||
* Display a success toast notification with green color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const success = createToast('success')
|
||||
|
||||
/**
|
||||
* Display a warning toast notification with yellow color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const warning = createToast('warning')
|
||||
|
||||
/**
|
||||
* Display an info toast notification with default color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const info = createToast('default')
|
||||
|
||||
/**
|
||||
* Display a loading toast notification that resolves with a promise
|
||||
* @param args - Toast options object containing a promise to resolve
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const loading = (args: RequireSome<AddToastProps, 'promise'>) => {
|
||||
// Disappear immediately by default
|
||||
if (args.timeout === undefined) {
|
||||
args.timeout = 1
|
||||
}
|
||||
return addToast(args)
|
||||
}
|
||||
|
||||
export const getToastUtilities = () =>
|
||||
({
|
||||
getToastQueue,
|
||||
addToast,
|
||||
closeToast,
|
||||
closeAll,
|
||||
isToastClosing,
|
||||
error,
|
||||
success,
|
||||
warning,
|
||||
info,
|
||||
loading
|
||||
}) as const
|
||||
231
src/renderer/src/components/TopView/toast.tsx
Normal file
231
src/renderer/src/components/TopView/toast.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import type { RequireSome } from '@renderer/types'
|
||||
import { message as antdMessage } from 'antd'
|
||||
import type { MessageInstance } from 'antd/es/message/interface'
|
||||
import type React from 'react'
|
||||
|
||||
// Global message instance for static usage
|
||||
let messageApi: MessageInstance | null = null
|
||||
|
||||
// Initialize message API - should be called once the App component is mounted
|
||||
export const initMessageApi = (api: MessageInstance) => {
|
||||
messageApi = api
|
||||
}
|
||||
|
||||
// Get message API instance
|
||||
const getMessageApi = (): MessageInstance => {
|
||||
if (!messageApi) {
|
||||
// Fallback to static method if hook API is not available
|
||||
return antdMessage
|
||||
}
|
||||
return messageApi
|
||||
}
|
||||
|
||||
type ToastColor = 'danger' | 'success' | 'warning' | 'default'
|
||||
type MessageType = 'error' | 'success' | 'warning' | 'info'
|
||||
|
||||
interface ToastConfig {
|
||||
title?: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
timeout?: number
|
||||
key?: string | number
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
onClick?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
interface LoadingToastConfig extends ToastConfig {
|
||||
promise: Promise<any>
|
||||
}
|
||||
|
||||
const colorToType = (color: ToastColor): MessageType => {
|
||||
switch (color) {
|
||||
case 'danger':
|
||||
return 'error'
|
||||
case 'success':
|
||||
return 'success'
|
||||
case 'warning':
|
||||
return 'warning'
|
||||
case 'default':
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
// Toast content component
|
||||
const ToastContent: React.FC<{ title?: React.ReactNode; description?: React.ReactNode; icon?: React.ReactNode }> = ({
|
||||
title,
|
||||
description,
|
||||
icon
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{(icon || title) && (
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
{icon}
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{description && <div className="text-sm">{description}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const createToast = (color: ToastColor) => {
|
||||
return (arg: ToastConfig | string): string | null => {
|
||||
const api = getMessageApi()
|
||||
const type = colorToType(color) as 'error' | 'success' | 'warning' | 'info'
|
||||
|
||||
if (typeof arg === 'string') {
|
||||
// antd message methods return a function to close the message
|
||||
api[type](arg)
|
||||
return null
|
||||
}
|
||||
|
||||
const { title, description, icon, timeout, ...restConfig } = arg
|
||||
|
||||
// Convert timeout from milliseconds to seconds (antd uses seconds)
|
||||
const duration = timeout !== undefined ? timeout / 1000 : 3
|
||||
|
||||
return (
|
||||
(api.open({
|
||||
type: type as 'error' | 'success' | 'warning' | 'info',
|
||||
content: <ToastContent title={title} description={description} icon={icon} />,
|
||||
duration,
|
||||
...restConfig
|
||||
}) as any) || null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an error toast notification with red color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const error = createToast('danger')
|
||||
|
||||
/**
|
||||
* Display a success toast notification with green color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const success = createToast('success')
|
||||
|
||||
/**
|
||||
* Display a warning toast notification with yellow color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const warning = createToast('warning')
|
||||
|
||||
/**
|
||||
* Display an info toast notification with default color
|
||||
* @param arg - Toast content (string) or toast options object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const info = createToast('default')
|
||||
|
||||
/**
|
||||
* Display a loading toast notification that resolves with a promise
|
||||
* @param args - Toast options object containing a promise to resolve
|
||||
*/
|
||||
export const loading = (args: RequireSome<LoadingToastConfig, 'promise'>): string | null => {
|
||||
const api = getMessageApi()
|
||||
const { title, description, icon, promise, timeout, ...restConfig } = args
|
||||
|
||||
// Generate unique key for this loading message
|
||||
const key = args.key || `loading-${Date.now()}-${Math.random()}`
|
||||
|
||||
// Show loading message
|
||||
api.loading({
|
||||
content: <ToastContent title={title || 'Loading...'} description={description} icon={icon} />,
|
||||
duration: 0, // Don't auto-close
|
||||
key,
|
||||
...restConfig
|
||||
})
|
||||
|
||||
// Handle promise resolution
|
||||
promise
|
||||
.then((result) => {
|
||||
api.success({
|
||||
content: <ToastContent title={title || 'Success'} description={description} />,
|
||||
duration: timeout !== undefined ? timeout / 1000 : 2,
|
||||
key,
|
||||
...restConfig
|
||||
})
|
||||
return result
|
||||
})
|
||||
.catch((err) => {
|
||||
api.error({
|
||||
content: (
|
||||
<ToastContent title={title || 'Error'} description={err?.message || description || 'An error occurred'} />
|
||||
),
|
||||
duration: timeout !== undefined ? timeout / 1000 : 3,
|
||||
key,
|
||||
...restConfig
|
||||
})
|
||||
throw err
|
||||
})
|
||||
|
||||
return key as string
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a toast notification
|
||||
* @param config - Toast configuration object
|
||||
* @returns Toast ID or null
|
||||
*/
|
||||
export const addToast = (config: ToastConfig) => info(config)
|
||||
|
||||
/**
|
||||
* Close a specific toast notification by its key
|
||||
* @param key - Toast key (string)
|
||||
*/
|
||||
export const closeToast = (key: string) => {
|
||||
getMessageApi().destroy(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close all toast notifications
|
||||
*/
|
||||
export const closeAll = () => {
|
||||
getMessageApi().destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub functions for compatibility with previous toast API
|
||||
* These are no-ops since antd message doesn't expose a queue
|
||||
*/
|
||||
|
||||
/**
|
||||
* @deprecated This function is a no-op stub for backward compatibility only.
|
||||
* Antd message doesn't expose a queue. Do not rely on this function.
|
||||
* @returns Empty toast queue stub
|
||||
*/
|
||||
export const getToastQueue = (): any => ({ toasts: [] })
|
||||
|
||||
/**
|
||||
* @deprecated This function is a no-op stub for backward compatibility only.
|
||||
* Antd message doesn't track closing state. Do not rely on this function.
|
||||
* @param key - Toast key (unused)
|
||||
* @returns Always returns false
|
||||
*/
|
||||
export const isToastClosing = (key?: string): boolean => {
|
||||
key // unused
|
||||
return false
|
||||
}
|
||||
|
||||
export const getToastUtilities = () =>
|
||||
({
|
||||
getToastQueue,
|
||||
addToast,
|
||||
closeToast,
|
||||
closeAll,
|
||||
isToastClosing,
|
||||
error,
|
||||
success,
|
||||
warning,
|
||||
info,
|
||||
loading
|
||||
}) as const
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ScrollShadow } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { handleSaveData } from '@renderer/store'
|
||||
import type { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Markdown from 'react-markdown'
|
||||
|
||||
const logger = loggerService.withContext('UpdateDialog')
|
||||
|
||||
interface UpdateDialogProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
releaseInfo: UpdateInfo | null
|
||||
}
|
||||
|
||||
const UpdateDialog: React.FC<UpdateDialogProps> = ({ isOpen, onClose, releaseInfo }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isInstalling, setIsInstalling] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && releaseInfo) {
|
||||
logger.info('Update dialog opened', { version: releaseInfo.version })
|
||||
}
|
||||
}, [isOpen, releaseInfo])
|
||||
|
||||
const handleInstall = async () => {
|
||||
setIsInstalling(true)
|
||||
try {
|
||||
await handleSaveData()
|
||||
await window.api.quitAndInstall()
|
||||
} catch (error) {
|
||||
logger.error('Failed to save data before update', error as Error)
|
||||
setIsInstalling(false)
|
||||
window.toast.error(t('update.saveDataError'))
|
||||
}
|
||||
}
|
||||
|
||||
const releaseNotes = releaseInfo?.releaseNotes
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
size="2xl"
|
||||
scrollBehavior="inside"
|
||||
classNames={{
|
||||
base: 'max-h-[85vh]',
|
||||
header: 'border-b border-divider',
|
||||
footer: 'border-t border-divider'
|
||||
}}>
|
||||
<ModalContent>
|
||||
{(onModalClose) => (
|
||||
<>
|
||||
<ModalHeader className="flex flex-col gap-1">
|
||||
<h3 className="font-semibold text-lg">{t('update.title')}</h3>
|
||||
<p className="text-default-500 text-small">
|
||||
{t('update.message').replace('{{version}}', releaseInfo?.version || '')}
|
||||
</p>
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<ScrollShadow className="max-h-[450px]" hideScrollBar>
|
||||
<div className="markdown rounded-lg bg-default-50 p-4">
|
||||
<Markdown>
|
||||
{typeof releaseNotes === 'string'
|
||||
? releaseNotes
|
||||
: Array.isArray(releaseNotes)
|
||||
? releaseNotes
|
||||
.map((note: ReleaseNoteInfo) => note.note)
|
||||
.filter(Boolean)
|
||||
.join('\n\n')
|
||||
: t('update.noReleaseNotes')}
|
||||
</Markdown>
|
||||
</div>
|
||||
</ScrollShadow>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onModalClose} isDisabled={isInstalling}>
|
||||
{t('update.later')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={async () => {
|
||||
await handleInstall()
|
||||
onModalClose()
|
||||
}}
|
||||
isLoading={isInstalling}>
|
||||
{t('update.install')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateDialog
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { SelectedItems, SelectProps } from '@heroui/react'
|
||||
import { Chip, cn, Select, SelectItem } from '@heroui/react'
|
||||
import type { Tool } from '@renderer/types'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface AllowedToolsSelectProps extends Omit<SelectProps, 'children'> {
|
||||
items: Tool[]
|
||||
}
|
||||
|
||||
export const AllowedToolsSelect: React.FC<AllowedToolsSelectProps> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { items: availableTools, className, ...rest } = props
|
||||
|
||||
const renderSelectedTools = useCallback((items: SelectedItems<Tool>) => {
|
||||
if (!items.length) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{items.map((item) => (
|
||||
<Chip key={item.key} size="sm" variant="flat" className="max-w-[160px] truncate">
|
||||
{item.data?.name ?? item.textValue ?? item.key}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Select
|
||||
aria-label={t('agent.session.allowed_tools.label')}
|
||||
selectionMode="multiple"
|
||||
isMultiline
|
||||
label={t('agent.session.allowed_tools.label')}
|
||||
placeholder={t('agent.session.allowed_tools.placeholder')}
|
||||
description={
|
||||
availableTools.length ? t('agent.session.allowed_tools.helper') : t('agent.session.allowed_tools.empty')
|
||||
}
|
||||
isDisabled={!availableTools.length}
|
||||
items={availableTools}
|
||||
renderValue={renderSelectedTools}
|
||||
className={cn('max-w-xl', className)}
|
||||
{...rest}>
|
||||
{(tool) => (
|
||||
<SelectItem key={tool.id} textValue={tool.name}>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium text-sm">{tool.name}</span>
|
||||
{tool.description ? <span className="text-foreground-500 text-xs">{tool.description}</span> : null}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { AllowedToolsSelect } from './AllowedToolsSelect'
|
||||
@@ -1490,6 +1490,10 @@ export function isCherryAIProvider(provider: Provider): boolean {
|
||||
return provider.id === 'cherryai'
|
||||
}
|
||||
|
||||
export function isPerplexityProvider(provider: Provider): boolean {
|
||||
return provider.id === 'perplexity'
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为 OpenAI 兼容的提供商
|
||||
* @param {Provider} provider 提供商对象
|
||||
@@ -1515,7 +1519,7 @@ export function isGeminiProvider(provider: Provider): boolean {
|
||||
return provider.type === 'gemini'
|
||||
}
|
||||
|
||||
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot'] as const satisfies SystemProviderId[]
|
||||
const NOT_SUPPORT_API_VERSION_PROVIDERS = ['github', 'copilot', 'perplexity'] as const satisfies SystemProviderId[]
|
||||
|
||||
export const isSupportAPIVersionProvider = (provider: Provider) => {
|
||||
if (isSystemProvider(provider)) {
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { HeroUIProvider } from '@heroui/react'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
|
||||
const AppHeroUIProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { language } = useSettings()
|
||||
return (
|
||||
<HeroUIProvider className="flex h-full w-full flex-1" locale={language}>
|
||||
{children}
|
||||
</HeroUIProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export { AppHeroUIProvider as HeroUIProvider }
|
||||
14
src/renderer/src/env.d.ts
vendored
14
src/renderer/src/env.d.ts
vendored
@@ -1,12 +1,22 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import type { addToast, closeAll, closeToast, getToastQueue, isToastClosing } from '@heroui/toast'
|
||||
import type KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import type { HookAPI } from 'antd/es/modal/useModal'
|
||||
import type { NavigateFunction } from 'react-router-dom'
|
||||
|
||||
import type { error, info, loading, success, warning } from './components/TopView/toast'
|
||||
import type {
|
||||
addToast,
|
||||
closeAll,
|
||||
closeToast,
|
||||
error,
|
||||
getToastQueue,
|
||||
info,
|
||||
isToastClosing,
|
||||
loading,
|
||||
success,
|
||||
warning
|
||||
} from './components/TopView/toast'
|
||||
|
||||
interface ImportMetaEnv {
|
||||
VITE_RENDERER_INTEGRATED_MODEL: string
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
import { heroui } from '@heroui/react'
|
||||
export default heroui()
|
||||
@@ -41,6 +41,7 @@ export const useAgents = () => {
|
||||
// NOTE: We only use the array for now. useUpdateAgent depends on this behavior.
|
||||
return result.data
|
||||
}, [apiServerConfig.enabled, apiServerRunning, client, t])
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR(swrKey, fetcher)
|
||||
const { chat } = useRuntime()
|
||||
const { activeAgentId } = chat
|
||||
|
||||
@@ -31,21 +31,24 @@ export const useApiServer = () => {
|
||||
try {
|
||||
const status = await window.api.apiServer.getStatus()
|
||||
setApiServerRunning(status.running)
|
||||
if (status.running && !apiServerConfig.enabled) {
|
||||
setApiServerEnabled(true)
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to check API server status:', error)
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [])
|
||||
}, [apiServerConfig.enabled, setApiServerEnabled])
|
||||
|
||||
const startApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.start()
|
||||
if (result.success) {
|
||||
setApiServerRunning(true)
|
||||
setApiServerEnabled(true)
|
||||
window.toast.success(t('apiServer.messages.startSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.startError') + result.error)
|
||||
@@ -55,16 +58,16 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, t])
|
||||
}, [apiServerLoading, setApiServerEnabled, t])
|
||||
|
||||
const stopApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.stop()
|
||||
if (result.success) {
|
||||
setApiServerRunning(false)
|
||||
setApiServerEnabled(false)
|
||||
window.toast.success(t('apiServer.messages.stopSuccess'))
|
||||
} else {
|
||||
window.toast.error(t('apiServer.messages.stopError') + result.error)
|
||||
@@ -74,14 +77,14 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, t])
|
||||
}, [apiServerLoading, setApiServerEnabled, t])
|
||||
|
||||
const restartApiServer = useCallback(async () => {
|
||||
if (apiServerLoading) return
|
||||
|
||||
setApiServerLoading(true)
|
||||
try {
|
||||
const result = await window.api.apiServer.restart()
|
||||
setApiServerEnabled(result.success)
|
||||
if (result.success) {
|
||||
await checkApiServerStatus()
|
||||
window.toast.success(t('apiServer.messages.restartSuccess'))
|
||||
@@ -93,7 +96,7 @@ export const useApiServer = () => {
|
||||
} finally {
|
||||
setApiServerLoading(false)
|
||||
}
|
||||
}, [apiServerLoading, checkApiServerStatus, t])
|
||||
}, [apiServerLoading, checkApiServerStatus, setApiServerEnabled, t])
|
||||
|
||||
useEffect(() => {
|
||||
checkApiServerStatus()
|
||||
|
||||
@@ -221,13 +221,12 @@ export function useAppInit() {
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Request, requestListener)
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
const removeListeners = [
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Request, requestListener),
|
||||
window.electron.ipcRenderer.on(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
]
|
||||
|
||||
return () => {
|
||||
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Request, requestListener)
|
||||
window.electron?.ipcRenderer.removeListener(IpcChannel.AgentToolPermission_Result, resultListener)
|
||||
}
|
||||
return () => removeListeners.forEach((removeListener) => removeListener())
|
||||
}, [dispatch, t])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -12,19 +12,7 @@ export default function useUserTheme() {
|
||||
const colorPrimary = Color(theme.colorPrimary)
|
||||
|
||||
document.body.style.setProperty('--color-primary', colorPrimary.toString())
|
||||
// overwrite hero UI primary color.
|
||||
document.body.style.setProperty('--primary', colorPrimary.toString())
|
||||
document.body.style.setProperty('--heroui-primary', colorPrimary.toString())
|
||||
document.body.style.setProperty('--heroui-primary-900', colorPrimary.lighten(0.5).toString())
|
||||
document.body.style.setProperty('--heroui-primary-800', colorPrimary.lighten(0.4).toString())
|
||||
document.body.style.setProperty('--heroui-primary-700', colorPrimary.lighten(0.3).toString())
|
||||
document.body.style.setProperty('--heroui-primary-600', colorPrimary.lighten(0.2).toString())
|
||||
document.body.style.setProperty('--heroui-primary-500', colorPrimary.lighten(0.1).toString())
|
||||
document.body.style.setProperty('--heroui-primary-400', colorPrimary.toString())
|
||||
document.body.style.setProperty('--heroui-primary-300', colorPrimary.darken(0.1).toString())
|
||||
document.body.style.setProperty('--heroui-primary-200', colorPrimary.darken(0.2).toString())
|
||||
document.body.style.setProperty('--heroui-primary-100', colorPrimary.darken(0.3).toString())
|
||||
document.body.style.setProperty('--heroui-primary-50', colorPrimary.darken(0.4).toString())
|
||||
document.body.style.setProperty('--color-primary-soft', colorPrimary.alpha(0.6).toString())
|
||||
document.body.style.setProperty('--color-primary-mute', colorPrimary.alpha(0.3).toString())
|
||||
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "API Server"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "About",
|
||||
"close": "Close Window",
|
||||
"copy": "Copy",
|
||||
"cut": "Cut",
|
||||
"delete": "Delete",
|
||||
"documentation": "Documentation",
|
||||
"edit": "Edit",
|
||||
"feedback": "Feedback",
|
||||
"file": "File",
|
||||
"forceReload": "Force Reload",
|
||||
"front": "Bring All to Front",
|
||||
"help": "Help",
|
||||
"hide": "Hide",
|
||||
"hideOthers": "Hide Others",
|
||||
"minimize": "Minimize",
|
||||
"paste": "Paste",
|
||||
"quit": "Quit",
|
||||
"redo": "Redo",
|
||||
"releases": "Releases",
|
||||
"reload": "Reload",
|
||||
"resetZoom": "Actual Size",
|
||||
"selectAll": "Select All",
|
||||
"services": "Services",
|
||||
"toggleDevTools": "Toggle Developer Tools",
|
||||
"toggleFullscreen": "Toggle Fullscreen",
|
||||
"undo": "Undo",
|
||||
"unhide": "Show All",
|
||||
"view": "View",
|
||||
"website": "Website",
|
||||
"window": "Window",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Assistants",
|
||||
"clear": {
|
||||
@@ -3766,6 +3801,7 @@
|
||||
"description": "Do not enable MCP server functionality",
|
||||
"label": "Disable MCP Server"
|
||||
},
|
||||
"discover": "Discover",
|
||||
"duplicateName": "A server with this name already exists",
|
||||
"editJson": "Edit JSON",
|
||||
"editMcpJson": "Edit MCP Configuration",
|
||||
@@ -3776,6 +3812,10 @@
|
||||
"32000": "MCP server failed to start, please check the parameters according to the tutorial",
|
||||
"toolNotFound": "Tool {{name}} not found"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Fetch Servers",
|
||||
"success": "Successfully fetched MCP servers"
|
||||
},
|
||||
"findMore": "Find More MCP",
|
||||
"headers": "Headers",
|
||||
"headersTooltip": "Custom headers for HTTP requests",
|
||||
@@ -3791,6 +3831,7 @@
|
||||
"logoUrl": "Logo URL",
|
||||
"longRunning": "Long Running Mode",
|
||||
"longRunningTooltip": "When enabled, the server supports long-running tasks. When receiving progress notifications, the timeout will be reset and the maximum execution time will be extended to 10 minutes.",
|
||||
"marketplaces": "Marketplaces",
|
||||
"missingDependencies": "is Missing, please install it to continue.",
|
||||
"more": {
|
||||
"awesome": "Curated MCP Server List",
|
||||
@@ -3839,6 +3880,7 @@
|
||||
"provider": "Provider",
|
||||
"providerPlaceholder": "Provider name",
|
||||
"providerUrl": "Provider URL",
|
||||
"providers": "Providers",
|
||||
"registry": "Package Registry",
|
||||
"registryDefault": "Default",
|
||||
"registryTooltip": "Choose the registry for package installation to resolve network issues with the default registry.",
|
||||
@@ -3861,6 +3903,7 @@
|
||||
"searchNpx": "Search MCP",
|
||||
"serverPlural": "servers",
|
||||
"serverSingular": "server",
|
||||
"servers": "MCP Servers",
|
||||
"sse": "Server-Sent Events (sse)",
|
||||
"startError": "Start failed",
|
||||
"stdio": "Standard Input/Output (stdio)",
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "API 服务器"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "关于",
|
||||
"close": "关闭窗口",
|
||||
"copy": "复制",
|
||||
"cut": "剪切",
|
||||
"delete": "删除",
|
||||
"documentation": "文档",
|
||||
"edit": "编辑",
|
||||
"feedback": "反馈",
|
||||
"file": "文件",
|
||||
"forceReload": "强制重新加载",
|
||||
"front": "全部置于顶层",
|
||||
"help": "帮助",
|
||||
"hide": "隐藏",
|
||||
"hideOthers": "隐藏其他",
|
||||
"minimize": "最小化",
|
||||
"paste": "粘贴",
|
||||
"quit": "退出",
|
||||
"redo": "重做",
|
||||
"releases": "版本发布",
|
||||
"reload": "重新加载",
|
||||
"resetZoom": "实际大小",
|
||||
"selectAll": "全选",
|
||||
"services": "服务",
|
||||
"toggleDevTools": "切换开发者工具",
|
||||
"toggleFullscreen": "切换全屏",
|
||||
"undo": "撤销",
|
||||
"unhide": "全部显示",
|
||||
"view": "视图",
|
||||
"website": "网站",
|
||||
"window": "窗口",
|
||||
"zoom": "缩放",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "助手",
|
||||
"clear": {
|
||||
@@ -1611,7 +1646,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "智能体添加成功"
|
||||
"content": "助手添加成功"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
@@ -3766,6 +3801,7 @@
|
||||
"description": "不启用 MCP 服务功能",
|
||||
"label": "不使用 MCP 服务器"
|
||||
},
|
||||
"discover": "发现",
|
||||
"duplicateName": "已存在同名服务器",
|
||||
"editJson": "编辑 JSON",
|
||||
"editMcpJson": "编辑 MCP 配置",
|
||||
@@ -3776,6 +3812,10 @@
|
||||
"32000": "MCP 服务器启动失败,请根据教程检查参数是否填写完整",
|
||||
"toolNotFound": "未找到工具 {{name}}"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "获取服务器",
|
||||
"success": "服务器获取成功"
|
||||
},
|
||||
"findMore": "更多 MCP",
|
||||
"headers": "请求头",
|
||||
"headersTooltip": "HTTP 请求的自定义请求头",
|
||||
@@ -3791,6 +3831,7 @@
|
||||
"logoUrl": "标志网址",
|
||||
"longRunning": "长时间运行模式",
|
||||
"longRunningTooltip": "启用后,服务器支持长时间任务,接收到进度通知时会重置超时计时器,并延长最大超时时间至10分钟",
|
||||
"marketplaces": "市场",
|
||||
"missingDependencies": "缺失,请安装它以继续",
|
||||
"more": {
|
||||
"awesome": "精选的 MCP 服务器列表",
|
||||
@@ -3839,6 +3880,7 @@
|
||||
"provider": "提供者",
|
||||
"providerPlaceholder": "提供者名称",
|
||||
"providerUrl": "提供者网址",
|
||||
"providers": "提供商",
|
||||
"registry": "包管理源",
|
||||
"registryDefault": "默认",
|
||||
"registryTooltip": "选择用于安装包的源,以解决默认源的网络问题",
|
||||
@@ -3861,6 +3903,7 @@
|
||||
"searchNpx": "搜索 MCP",
|
||||
"serverPlural": "服务器",
|
||||
"serverSingular": "服务器",
|
||||
"servers": "MCP 服务器",
|
||||
"sse": "服务器发送事件 (sse)",
|
||||
"startError": "启动失败",
|
||||
"stdio": "标准输入 / 输出 (stdio)",
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "API 伺服器"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "關於",
|
||||
"close": "關閉視窗",
|
||||
"copy": "複製",
|
||||
"cut": "剪下",
|
||||
"delete": "刪除",
|
||||
"documentation": "文件",
|
||||
"edit": "編輯",
|
||||
"feedback": "回饋",
|
||||
"file": "檔案",
|
||||
"forceReload": "強制重新載入",
|
||||
"front": "全部置於頂層",
|
||||
"help": "幫助",
|
||||
"hide": "隱藏",
|
||||
"hideOthers": "隱藏其他",
|
||||
"minimize": "最小化",
|
||||
"paste": "貼上",
|
||||
"quit": "結束",
|
||||
"redo": "重做",
|
||||
"releases": "版本發布",
|
||||
"reload": "重新載入",
|
||||
"resetZoom": "實際大小",
|
||||
"selectAll": "全選",
|
||||
"services": "服務",
|
||||
"toggleDevTools": "切換開發者工具",
|
||||
"toggleFullscreen": "切換全螢幕",
|
||||
"undo": "復原",
|
||||
"unhide": "全部顯示",
|
||||
"view": "檢視",
|
||||
"website": "網站",
|
||||
"window": "視窗",
|
||||
"zoom": "縮放",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "縮小"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "助手",
|
||||
"clear": {
|
||||
@@ -1611,7 +1646,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "智慧代理人新增成功"
|
||||
"content": "助手新增成功"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
@@ -3766,6 +3801,7 @@
|
||||
"description": "不啟用 MCP 服務功能",
|
||||
"label": "不使用 MCP 伺服器"
|
||||
},
|
||||
"discover": "發現",
|
||||
"duplicateName": "已存在相同名稱的伺服器",
|
||||
"editJson": "編輯 JSON",
|
||||
"editMcpJson": "編輯 MCP 配置",
|
||||
@@ -3776,6 +3812,10 @@
|
||||
"32000": "MCP 伺服器啟動失敗,請根據教程檢查參數是否填寫完整",
|
||||
"toolNotFound": "未找到工具 {{name}}"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "獲取伺服器",
|
||||
"success": "伺服器獲取成功"
|
||||
},
|
||||
"findMore": "更多 MCP",
|
||||
"headers": "請求標頭",
|
||||
"headersTooltip": "HTTP 請求的自定義標頭",
|
||||
@@ -3791,6 +3831,7 @@
|
||||
"logoUrl": "標誌網址",
|
||||
"longRunning": "長時間運行模式",
|
||||
"longRunningTooltip": "啟用後,伺服器支援長時間任務,接收到進度通知時會重置超時計時器,並延長最大超時時間至10分鐘",
|
||||
"marketplaces": "市場",
|
||||
"missingDependencies": "缺失,請安裝它以繼續",
|
||||
"more": {
|
||||
"awesome": "精選的 MCP 伺服器清單",
|
||||
@@ -3839,6 +3880,7 @@
|
||||
"provider": "提供者",
|
||||
"providerPlaceholder": "提供者名稱",
|
||||
"providerUrl": "提供者網址",
|
||||
"providers": "提供商",
|
||||
"registry": "套件管理源",
|
||||
"registryDefault": "預設",
|
||||
"registryTooltip": "選擇用於安裝套件的源,以解決預設源的網路問題",
|
||||
@@ -3861,6 +3903,7 @@
|
||||
"searchNpx": "搜索 MCP",
|
||||
"serverPlural": "伺服器",
|
||||
"serverSingular": "伺服器",
|
||||
"servers": "MCP 伺服器",
|
||||
"sse": "伺服器傳送事件 (sse)",
|
||||
"startError": "啟動失敗",
|
||||
"stdio": "標準輸入 / 輸出 (stdio)",
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "API-Server"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "Über",
|
||||
"close": "Fenster schließen",
|
||||
"copy": "Kopieren",
|
||||
"cut": "Schneiden",
|
||||
"delete": "Löschen",
|
||||
"documentation": "Dokumentation",
|
||||
"edit": "Bearbeiten",
|
||||
"feedback": "Rückmeldung",
|
||||
"file": "Datei",
|
||||
"forceReload": "Neu laden erzwingen",
|
||||
"front": "Alle in den Vordergrund bringen",
|
||||
"help": "Hilfe",
|
||||
"hide": "Verstecken",
|
||||
"hideOthers": "Andere ausblenden",
|
||||
"minimize": "Minimieren",
|
||||
"paste": "Einfügen",
|
||||
"quit": "Aufhören",
|
||||
"redo": "Wiederholen",
|
||||
"releases": "Veröffentlichungen",
|
||||
"reload": "Neu laden",
|
||||
"resetZoom": "Tatsächliche Größe",
|
||||
"selectAll": "Alle auswählen",
|
||||
"services": "Dienstleistungen",
|
||||
"toggleDevTools": "Entwicklertools ein-/ausblenden",
|
||||
"toggleFullscreen": "Vollbild umschalten",
|
||||
"undo": "Rückgängig machen",
|
||||
"unhide": "Alle anzeigen",
|
||||
"view": "Ansicht",
|
||||
"website": "Website",
|
||||
"window": "Fenster",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Heranzoomen",
|
||||
"zoomOut": "Herauszoomen"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Assistent",
|
||||
"clear": {
|
||||
@@ -1611,7 +1646,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "Agent erfolgreich hinzugefügt"
|
||||
"content": "Assistent erfolgreich hinzugefügt"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
@@ -3766,6 +3801,7 @@
|
||||
"description": "MCP-Service-Funktion nicht aktivieren",
|
||||
"label": "MCP-Server nicht verwenden"
|
||||
},
|
||||
"discover": "Entdecken",
|
||||
"duplicateName": "Server mit gleichem Namen existiert bereits",
|
||||
"editJson": "JSON bearbeiten",
|
||||
"editMcpJson": "MCP-Konfiguration bearbeiten",
|
||||
@@ -3776,6 +3812,10 @@
|
||||
"32000": "MCP-Server starten fehlgeschlagen, bitte überprüfen Sie, ob alle Parameter vollständig ausgefüllt sind",
|
||||
"toolNotFound": "Tool {{name}} nicht gefunden"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Server abrufen",
|
||||
"success": "MCP-Server erfolgreich abgerufen"
|
||||
},
|
||||
"findMore": "Mehr MCP",
|
||||
"headers": "Request-Header",
|
||||
"headersTooltip": "Benutzerdefinierte Request-Header für HTTP-Anfragen",
|
||||
@@ -3791,6 +3831,7 @@
|
||||
"logoUrl": "Logo-URL",
|
||||
"longRunning": "Lang laufender Modus",
|
||||
"longRunningTooltip": "Nach Aktivierung unterstützt der Server lange Aufgaben. Wenn ein Fortschrittsbenachrichtigung empfangen wird, wird der Timeout-Timer zurückgesetzt und die maximale Timeout-Zeit auf 10 Minuten verlängert",
|
||||
"marketplaces": "Marktplätze",
|
||||
"missingDependencies": "Abhängigkeiten fehlen, bitte installieren Sie sie, um fortzufahren",
|
||||
"more": {
|
||||
"awesome": "Kuratierte MCP-Serverliste",
|
||||
@@ -3839,6 +3880,7 @@
|
||||
"provider": "Anbieter",
|
||||
"providerPlaceholder": "Anbietername",
|
||||
"providerUrl": "Anbieter-Website",
|
||||
"providers": "Anbieter",
|
||||
"registry": "Paketverwaltungsquelle",
|
||||
"registryDefault": "Standard",
|
||||
"registryTooltip": "Quelle für Paketinstallation auswählen um Netzwerkprobleme der Standardquelle zu lösen",
|
||||
@@ -3861,6 +3903,7 @@
|
||||
"searchNpx": "MCP durchsuchen",
|
||||
"serverPlural": "Server",
|
||||
"serverSingular": "Server",
|
||||
"servers": "MCP-Server",
|
||||
"sse": "Server-Sende-Ereignisse (sse)",
|
||||
"startError": "Start fehlgeschlagen",
|
||||
"stdio": "Standard-Eingabe / -Ausgabe (stdio)",
|
||||
@@ -4260,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS-Zugriffsschlüssel-ID",
|
||||
"access_key_id_help": "Ihre AWS-Zugriffsschlüssel-ID, um auf AWS Bedrock-Dienste zuzugreifen",
|
||||
"api_key": "Bedrock-API-Schlüssel",
|
||||
"api_key_help": "Ihr AWS Bedrock-API-Schlüssel für die Authentifizierung",
|
||||
"auth_type": "Authentifizierungstyp",
|
||||
"auth_type_api_key": "Bedrock-API-Schlüssel",
|
||||
"auth_type_help": "Wählen Sie zwischen IAM-Anmeldeinformationen oder Bedrock-API-Schlüssel-Authentifizierung",
|
||||
"auth_type_iam": "IAM-Anmeldeinformationen",
|
||||
"description": "AWS Bedrock ist ein vollständig verwalteter Basismodell-Dienst von Amazon, der eine Vielzahl moderner großer Sprachmodelle unterstützt",
|
||||
"region": "AWS-Region",
|
||||
"region_help": "Ihre AWS-Serviceregion, z.B. us-east-1",
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "Διακομιστής API"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "Σχετικά",
|
||||
"close": "Κλείσιμο Παραθύρου",
|
||||
"copy": "Αντιγραφή",
|
||||
"cut": "Κόψε",
|
||||
"delete": "Διαγραφή",
|
||||
"documentation": "Τεκμηρίωση",
|
||||
"edit": "Επεξεργασία",
|
||||
"feedback": "Σχόλια",
|
||||
"file": "Αρχείο",
|
||||
"forceReload": "Εξαναγκασμένη επαναφόρτωση",
|
||||
"front": "Μεταφορά Όλων Μπροστά",
|
||||
"help": "Βοήθεια",
|
||||
"hide": "Κρύψε",
|
||||
"hideOthers": "Απόκρυψη Άλλων",
|
||||
"minimize": "Ελαχιστοποίηση",
|
||||
"paste": "Επικόλληση",
|
||||
"quit": "Παραιτήσου",
|
||||
"redo": "Ξανακάνε",
|
||||
"releases": "Κυκλοφορίες",
|
||||
"reload": "Επαναφόρτωση",
|
||||
"resetZoom": "Πραγματικό Μέγεθος",
|
||||
"selectAll": "Επιλογή Όλων",
|
||||
"services": "Υπηρεσίες",
|
||||
"toggleDevTools": "Εναλλαγή Εργαλείων Προγραμματιστή",
|
||||
"toggleFullscreen": "Εναλλαγή πλήρους οθόνης",
|
||||
"undo": "Αναίρεση",
|
||||
"unhide": "Εμφάνιση Όλων",
|
||||
"view": "Προβολή",
|
||||
"website": "Ιστοσελίδα",
|
||||
"window": "Παράθυρο",
|
||||
"zoom": "Ζουμ",
|
||||
"zoomIn": "Μεγέθυνση",
|
||||
"zoomOut": "Σμίκρυνση"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Βοηθός",
|
||||
"clear": {
|
||||
@@ -1611,7 +1646,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "Ο ενεργοποιημένος αστρόναυτης προστέθηκε επιτυχώς"
|
||||
"content": "Ο βοηθός προστέθηκε επιτυχώς"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
@@ -3766,6 +3801,7 @@
|
||||
"description": "Να μην ενεργοποιείται η λειτουργία υπηρεσίας MCP",
|
||||
"label": "Να μην χρησιμοποιείται διακομιστής MCP"
|
||||
},
|
||||
"discover": "Ανακαλύψτε",
|
||||
"duplicateName": "Υπάρχει ήδη ένας διακομιστής με αυτό το όνομα",
|
||||
"editJson": "Επεξεργασία JSON",
|
||||
"editMcpJson": "Επεξεργασία ρύθμισης MCP",
|
||||
@@ -3776,6 +3812,10 @@
|
||||
"32000": "Η εκκίνηση του MCP απέτυχε. Παρακαλώ ελέγξτε αν όλες οι παράμετροι έχουν συμπληρωθεί σύμφωνα με τον οδηγό.",
|
||||
"toolNotFound": "Δεν βρέθηκε το εργαλείο {{name}}"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Λήψη Διακομιστών",
|
||||
"success": "Επιτυχής ανάκτηση διακομιστών MCP"
|
||||
},
|
||||
"findMore": "Περισσότεροι διακομιστές MCP",
|
||||
"headers": "Κεφαλίδες",
|
||||
"headersTooltip": "Προσαρμοσμένες κεφαλίδες HTTP αιτήσεων",
|
||||
@@ -3791,6 +3831,7 @@
|
||||
"logoUrl": "URL Λογότυπου",
|
||||
"longRunning": "Μακροχρόνια λειτουργία",
|
||||
"longRunningTooltip": "Όταν ενεργοποιηθεί, ο διακομιστής υποστηρίζει μακροχρόνιες εργασίες, επαναφέρει το χρονικό όριο μετά από λήψη ειδοποίησης προόδου και επεκτείνει το μέγιστο χρονικό όριο σε 10 λεπτά.",
|
||||
"marketplaces": "Αγορές",
|
||||
"missingDependencies": "Λείπει, παρακαλώ εγκαταστήστε το για να συνεχίσετε",
|
||||
"more": {
|
||||
"awesome": "Επιλεγμένος κατάλογος διακομιστών MCP",
|
||||
@@ -3839,6 +3880,7 @@
|
||||
"provider": "Πάροχος",
|
||||
"providerPlaceholder": "Όνομα παρόχου",
|
||||
"providerUrl": "URL Παρόχου",
|
||||
"providers": "Πάροχοι",
|
||||
"registry": "Πηγή Διαχείρισης πακέτων",
|
||||
"registryDefault": "Προεπιλεγμένη",
|
||||
"registryTooltip": "Επιλέξτε την πηγή για την εγκατάσταση πακέτων, για να αντιμετωπιστούν προβλήματα δικτύου από την προεπιλεγμένη πηγή.",
|
||||
@@ -3861,6 +3903,7 @@
|
||||
"searchNpx": "Αναζήτηση MCP",
|
||||
"serverPlural": "Διακομιστές",
|
||||
"serverSingular": "Διακομιστής",
|
||||
"servers": "Διακομιστές MCP",
|
||||
"sse": "Συμβάντα Αποστολής από τον Διακομιστή (sse)",
|
||||
"startError": "Εκκίνηση Απέτυχε",
|
||||
"stdio": "Πρότυπη Είσοδος/Έξοδος (stdio)",
|
||||
@@ -4260,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "Αναγνωριστικό κλειδιού πρόσβασης AWS",
|
||||
"access_key_id_help": "Το ID του κλειδιού πρόσβασης AWS που χρησιμοποιείται για την πρόσβαση στην υπηρεσία AWS Bedrock",
|
||||
"api_key": "Κλειδί API Bedrock",
|
||||
"api_key_help": "Το κλειδί API του AWS Bedrock για έλεγχο ταυτότητας",
|
||||
"auth_type": "Τύπος Πιστοποίησης",
|
||||
"auth_type_api_key": "Κλειδί API Bedrock",
|
||||
"auth_type_help": "Επιλέξτε μεταξύ πιστοποιητικών IAM ή πιστοποίησης με κλειδί API Bedrock",
|
||||
"auth_type_iam": "Διαπιστευτήρια IAM",
|
||||
"description": "Η AWS Bedrock είναι μια πλήρως διαχειριζόμενη υπηρεσία βασικών μοντέλων που παρέχεται από την Amazon και υποστηρίζει διάφορα προηγμένα μεγάλα γλωσσικά μοντέλα.",
|
||||
"region": "Περιοχές AWS",
|
||||
"region_help": "Η περιοχή υπηρεσίας AWS σας, για παράδειγμα us-east-1",
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "Servidor API"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "Acerca de",
|
||||
"close": "Cerrar ventana",
|
||||
"copy": "Copiar",
|
||||
"cut": "Cortar",
|
||||
"delete": "Eliminar",
|
||||
"documentation": "Documentación",
|
||||
"edit": "Editar",
|
||||
"feedback": "Retroalimentación",
|
||||
"file": "Archivo",
|
||||
"forceReload": "Forzar recarga",
|
||||
"front": "Traer todo al frente",
|
||||
"help": "Ayuda",
|
||||
"hide": "Ocultar",
|
||||
"hideOthers": "Ocultar Otros",
|
||||
"minimize": "Minimizar",
|
||||
"paste": "Pegar",
|
||||
"quit": "Abandonar",
|
||||
"redo": "Rehacer",
|
||||
"releases": "Lanzamientos",
|
||||
"reload": "Recargar",
|
||||
"resetZoom": "Tamaño Real",
|
||||
"selectAll": "Seleccionar todo",
|
||||
"services": "Servicios",
|
||||
"toggleDevTools": "Alternar herramientas de desarrollo",
|
||||
"toggleFullscreen": "Activar pantalla completa",
|
||||
"undo": "Deshacer",
|
||||
"unhide": "Mostrar todo",
|
||||
"view": "Vista",
|
||||
"website": "Sitio web",
|
||||
"window": "Ventana",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Acercar",
|
||||
"zoomOut": "Alejar"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Asistente",
|
||||
"clear": {
|
||||
@@ -3766,6 +3801,7 @@
|
||||
"description": "No habilitar funciones del servicio MCP",
|
||||
"label": "No utilizar servidor MCP"
|
||||
},
|
||||
"discover": "Descubrir",
|
||||
"duplicateName": "Ya existe un servidor con el mismo nombre",
|
||||
"editJson": "Editar JSON",
|
||||
"editMcpJson": "Editar configuración MCP",
|
||||
@@ -3776,6 +3812,10 @@
|
||||
"32000": "El servidor MCP no se pudo iniciar, verifique si los parámetros están completos según la guía",
|
||||
"toolNotFound": "Herramienta no encontrada {{name}}"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Obtener Servidores",
|
||||
"success": "Servidores MCP obtenidos con éxito"
|
||||
},
|
||||
"findMore": "Más servidores MCP",
|
||||
"headers": "Encabezados",
|
||||
"headersTooltip": "Encabezados personalizados para solicitudes HTTP",
|
||||
@@ -3791,6 +3831,7 @@
|
||||
"logoUrl": "URL del logotipo",
|
||||
"longRunning": "Modo de ejecución prolongada",
|
||||
"longRunningTooltip": "Una vez habilitado, el servidor admite tareas de larga duración, reinicia el temporizador de tiempo de espera al recibir notificaciones de progreso y amplía el tiempo máximo de espera hasta 10 minutos.",
|
||||
"marketplaces": "Mercados",
|
||||
"missingDependencies": "Faltan, instalelas para continuar",
|
||||
"more": {
|
||||
"awesome": "Lista seleccionada de servidores MCP",
|
||||
@@ -3839,6 +3880,7 @@
|
||||
"provider": "Proveedor",
|
||||
"providerPlaceholder": "Nombre del proveedor",
|
||||
"providerUrl": "URL del proveedor",
|
||||
"providers": "Proveedores",
|
||||
"registry": "Repositorio de paquetes",
|
||||
"registryDefault": "Predeterminado",
|
||||
"registryTooltip": "Seleccione un repositorio para instalar paquetes, útil para resolver problemas de red con el repositorio predeterminado.",
|
||||
@@ -3861,6 +3903,7 @@
|
||||
"searchNpx": "Buscar MCP",
|
||||
"serverPlural": "Servidores",
|
||||
"serverSingular": "Servidor",
|
||||
"servers": "Servidores MCP",
|
||||
"sse": "Eventos enviados por el servidor (sse)",
|
||||
"startError": "Inicio fallido",
|
||||
"stdio": "Entrada/Salida estándar (stdio)",
|
||||
@@ -4260,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "ID de clave de acceso de AWS",
|
||||
"access_key_id_help": "Su ID de clave de acceso de AWS, utilizado para acceder al servicio AWS Bedrock",
|
||||
"api_key": "Clave de API de Bedrock",
|
||||
"api_key_help": "Tu clave de API de AWS Bedrock para autenticación",
|
||||
"auth_type": "Tipo de autenticación",
|
||||
"auth_type_api_key": "Clave de API de Bedrock",
|
||||
"auth_type_help": "Elige entre credenciales IAM o autenticación con clave API de Bedrock",
|
||||
"auth_type_iam": "Credenciales de IAM",
|
||||
"description": "AWS Bedrock es un servicio de modelos fundamentales completamente gestionado proporcionado por Amazon, que admite diversos modelos avanzados de lenguaje de gran tamaño.",
|
||||
"region": "Región de AWS",
|
||||
"region_help": "Su región de servicio AWS, por ejemplo us-east-1",
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "Serveur API"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "À propos",
|
||||
"close": "Fermer la fenêtre",
|
||||
"copy": "Copier",
|
||||
"cut": "Couper",
|
||||
"delete": "Supprimer",
|
||||
"documentation": "Documentation",
|
||||
"edit": "Modifier",
|
||||
"feedback": "Retour d'information",
|
||||
"file": "Fichier",
|
||||
"forceReload": "Rechargement forcé",
|
||||
"front": "Tout ramener au premier plan",
|
||||
"help": "Aide",
|
||||
"hide": "Cacher",
|
||||
"hideOthers": "Masquer les autres",
|
||||
"minimize": "Minimiser",
|
||||
"paste": "Coller",
|
||||
"quit": "Quitter",
|
||||
"redo": "Refaire",
|
||||
"releases": "Sorties",
|
||||
"reload": "Recharger",
|
||||
"resetZoom": "Taille réelle",
|
||||
"selectAll": "Tout sélectionner",
|
||||
"services": "Services",
|
||||
"toggleDevTools": "Basculer les outils de développement",
|
||||
"toggleFullscreen": "Basculer en plein écran",
|
||||
"undo": "Annuler",
|
||||
"unhide": "Tout afficher",
|
||||
"view": "Vue",
|
||||
"website": "Site web",
|
||||
"window": "Fenêtre",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Zoom Avant",
|
||||
"zoomOut": "Dézoomer"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Aide",
|
||||
"clear": {
|
||||
@@ -3766,6 +3801,7 @@
|
||||
"description": "Désactiver les fonctionnalités du service MCP",
|
||||
"label": "Ne pas utiliser le serveur MCP"
|
||||
},
|
||||
"discover": "Découvrir",
|
||||
"duplicateName": "Un serveur portant le même nom existe déjà",
|
||||
"editJson": "Modifier le JSON",
|
||||
"editMcpJson": "Редактировать конфигурацию MCP",
|
||||
@@ -3776,6 +3812,10 @@
|
||||
"32000": "Échec du démarrage du serveur MCP, veuillez vérifier si tous les paramètres sont correctement remplis conformément au tutoriel",
|
||||
"toolNotFound": "Outil non trouvé {{name}}"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Récupérer les serveurs",
|
||||
"success": "Serveurs MCP récupérés avec succès"
|
||||
},
|
||||
"findMore": "Plus de serveurs MCP",
|
||||
"headers": "Заголовки запроса",
|
||||
"headersTooltip": "Пользовательские заголовки HTTP-запроса",
|
||||
@@ -3791,6 +3831,7 @@
|
||||
"logoUrl": "Адрес логотипа",
|
||||
"longRunning": "Mode d'exécution prolongée",
|
||||
"longRunningTooltip": "Une fois activé, le serveur prend en charge les tâches de longue durée, réinitialise le minuteur de temporisation à la réception des notifications de progression, et prolonge le délai d'expiration maximal à 10 minutes.",
|
||||
"marketplaces": "Places de marché",
|
||||
"missingDependencies": "Manquantes, veuillez les installer pour continuer",
|
||||
"more": {
|
||||
"awesome": "Liste sélectionnée de serveurs MCP",
|
||||
@@ -3839,6 +3880,7 @@
|
||||
"provider": "Поставщик",
|
||||
"providerPlaceholder": "Название поставщика",
|
||||
"providerUrl": "Адрес поставщика",
|
||||
"providers": "Fournisseurs",
|
||||
"registry": "Источник управления пакетами",
|
||||
"registryDefault": "По умолчанию",
|
||||
"registryTooltip": "Выберите источник для установки пакетов, чтобы решить проблемы с сетью по умолчанию.",
|
||||
@@ -3861,6 +3903,7 @@
|
||||
"searchNpx": "Поиск MCP",
|
||||
"serverPlural": "Serveurs",
|
||||
"serverSingular": "Serveur",
|
||||
"servers": "Serveurs MCP",
|
||||
"sse": "Серверные отправляемые события (sse)",
|
||||
"startError": "Ошибка запуска",
|
||||
"stdio": "Стандартный ввод/вывод (stdio)",
|
||||
@@ -4260,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "Identifiant de clé d'accès AWS",
|
||||
"access_key_id_help": "Votre identifiant de clé d'accès AWS, utilisé pour accéder au service AWS Bedrock",
|
||||
"api_key": "Clé API Bedrock",
|
||||
"api_key_help": "Votre clé API AWS Bedrock pour l'authentification",
|
||||
"auth_type": "Type d'authentification",
|
||||
"auth_type_api_key": "Clé API Bedrock",
|
||||
"auth_type_help": "Choisissez entre l'authentification par identifiants IAM ou par clé API Bedrock",
|
||||
"auth_type_iam": "Identifiants IAM",
|
||||
"description": "AWS Bedrock est un service de modèles de base entièrement géré proposé par Amazon, prenant en charge divers grands modèles linguistiques avancés.",
|
||||
"region": "Région AWS",
|
||||
"region_help": "Votre région de service AWS, par exemple us-east-1",
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "API サーバー"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "について",
|
||||
"close": "ウィンドウを閉じる",
|
||||
"copy": "コピー",
|
||||
"cut": "切る",
|
||||
"delete": "削除",
|
||||
"documentation": "ドキュメント",
|
||||
"edit": "編集",
|
||||
"feedback": "フィードバック",
|
||||
"file": "ファイル",
|
||||
"forceReload": "強制再読み込み",
|
||||
"front": "すべてを前面に移動",
|
||||
"help": "助け",
|
||||
"hide": "隠す",
|
||||
"hideOthers": "他を隠す",
|
||||
"minimize": "最小化",
|
||||
"paste": "ペースト",
|
||||
"quit": "やめる",
|
||||
"redo": "やり直し",
|
||||
"releases": "リリース",
|
||||
"reload": "リロード",
|
||||
"resetZoom": "実寸",
|
||||
"selectAll": "すべて選択",
|
||||
"services": "サービス",
|
||||
"toggleDevTools": "開発者ツールを切り替え",
|
||||
"toggleFullscreen": "全画面表示を切り替え",
|
||||
"undo": "元に戻す",
|
||||
"unhide": "すべて表示",
|
||||
"view": "表示",
|
||||
"website": "ウェブサイト",
|
||||
"window": "窓",
|
||||
"zoom": "ズーム",
|
||||
"zoomIn": "ズームイン",
|
||||
"zoomOut": "ズームアウト"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "アシスタント",
|
||||
"clear": {
|
||||
@@ -1611,7 +1646,7 @@
|
||||
},
|
||||
"assistant": {
|
||||
"added": {
|
||||
"content": "アシスタントが追加されました"
|
||||
"content": "助手が追加されました"
|
||||
}
|
||||
},
|
||||
"attachments": {
|
||||
@@ -3766,6 +3801,7 @@
|
||||
"description": "MCP機能を有効にしない",
|
||||
"label": "MCPサーバーを無効にする"
|
||||
},
|
||||
"discover": "発見",
|
||||
"duplicateName": "同じ名前のサーバーが既に存在します",
|
||||
"editJson": "JSONを編集",
|
||||
"editMcpJson": "MCP 設定を編集",
|
||||
@@ -3776,6 +3812,10 @@
|
||||
"32000": "MCP サーバーが起動しませんでした。パラメーターを確認してください",
|
||||
"toolNotFound": "ツール {{name}} が見つかりません"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "サーバーを取得",
|
||||
"success": "MCPサーバーの取得に成功しました"
|
||||
},
|
||||
"findMore": "MCP を見つける",
|
||||
"headers": "ヘッダー",
|
||||
"headersTooltip": "HTTP リクエストのカスタムヘッダー",
|
||||
@@ -3791,6 +3831,7 @@
|
||||
"logoUrl": "ロゴURL",
|
||||
"longRunning": "長時間運行モード",
|
||||
"longRunningTooltip": "このオプションを有効にすると、サーバーは長時間のタスクをサポートします。進行状況通知を受信すると、タイムアウトがリセットされ、最大実行時間が10分に延長されます。",
|
||||
"marketplaces": "マーケットプレイス",
|
||||
"missingDependencies": "が不足しています。続行するにはインストールしてください。",
|
||||
"more": {
|
||||
"awesome": "厳選された MCP サーバーリスト",
|
||||
@@ -3839,6 +3880,7 @@
|
||||
"provider": "プロバイダー",
|
||||
"providerPlaceholder": "プロバイダー名",
|
||||
"providerUrl": "プロバイダーURL",
|
||||
"providers": "プロバイダー",
|
||||
"registry": "パッケージ管理レジストリ",
|
||||
"registryDefault": "デフォルト",
|
||||
"registryTooltip": "デフォルトのレジストリでネットワークの問題が発生した場合、パッケージインストールに使用するレジストリを選択してください。",
|
||||
@@ -3861,6 +3903,7 @@
|
||||
"searchNpx": "MCP を検索",
|
||||
"serverPlural": "サーバー",
|
||||
"serverSingular": "サーバー",
|
||||
"servers": "MCPサーバー",
|
||||
"sse": "サーバー送信イベント (sse)",
|
||||
"startError": "起動に失敗しました",
|
||||
"stdio": "標準入力/出力 (stdio)",
|
||||
@@ -4260,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS アクセスキー ID",
|
||||
"access_key_id_help": "あなたの AWS アクセスキー ID は、AWS Bedrock サービスへのアクセスに使用されます",
|
||||
"api_key": "Bedrock APIキー",
|
||||
"api_key_help": "認証用のAWS Bedrock APIキー",
|
||||
"auth_type": "認証タイプ",
|
||||
"auth_type_api_key": "Bedrock APIキー",
|
||||
"auth_type_help": "IAM認証情報とBedrock APIキー認証のどちらかを選択してください",
|
||||
"auth_type_iam": "IAM認証情報",
|
||||
"description": "AWS Bedrock は、Amazon が提供する完全に管理されたベースモデルサービスで、さまざまな最先端の大言語モデルをサポートしています",
|
||||
"region": "AWS リージョン",
|
||||
"region_help": "あなたの AWS サービスリージョン、例:us-east-1",
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "Servidor API"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "Sobre",
|
||||
"close": "Fechar Janela",
|
||||
"copy": "Copiar",
|
||||
"cut": "Corte",
|
||||
"delete": "Excluir",
|
||||
"documentation": "Documentação",
|
||||
"edit": "Editar",
|
||||
"feedback": "Feedback",
|
||||
"file": "Arquivo",
|
||||
"forceReload": "Forçar Recarregamento",
|
||||
"front": "Trazer Tudo para a Frente",
|
||||
"help": "Ajuda",
|
||||
"hide": "Esconder",
|
||||
"hideOthers": "Ocultar Outros",
|
||||
"minimize": "Minimizar",
|
||||
"paste": "Colar",
|
||||
"quit": "Sair",
|
||||
"redo": "Refazer",
|
||||
"releases": "Lançamentos",
|
||||
"reload": "Recarregar",
|
||||
"resetZoom": "Tamanho Real",
|
||||
"selectAll": "Selecionar Todos",
|
||||
"services": "Serviços",
|
||||
"toggleDevTools": "Alternar Ferramentas de Desenvolvedor",
|
||||
"toggleFullscreen": "Alternar Tela Cheia",
|
||||
"undo": "Desfazer",
|
||||
"unhide": "Mostrar Todos",
|
||||
"view": "Visualizar",
|
||||
"website": "Site",
|
||||
"window": "Janela",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Ampliar",
|
||||
"zoomOut": "Reduzir Zoom"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Assistente",
|
||||
"clear": {
|
||||
@@ -3766,6 +3801,7 @@
|
||||
"description": "Não ativar a funcionalidade do serviço MCP",
|
||||
"label": "Não usar servidor MCP"
|
||||
},
|
||||
"discover": "Descobrir",
|
||||
"duplicateName": "Já existe um servidor com o mesmo nome",
|
||||
"editJson": "Editar JSON",
|
||||
"editMcpJson": "Editar Configuração MCP",
|
||||
@@ -3776,6 +3812,10 @@
|
||||
"32000": "Falha ao iniciar o servidor MCP, verifique se todos os parâmetros foram preenchidos corretamente conforme o tutorial",
|
||||
"toolNotFound": "Ferramenta não encontrada {{name}}"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Buscar Servidores",
|
||||
"success": "Servidores MCP obtidos com sucesso"
|
||||
},
|
||||
"findMore": "Mais servidores MCP",
|
||||
"headers": "Cabeçalhos da Requisição",
|
||||
"headersTooltip": "Cabeçalhos HTTP personalizados para as requisições",
|
||||
@@ -3791,6 +3831,7 @@
|
||||
"logoUrl": "URL do Logotipo",
|
||||
"longRunning": "Modo de execução prolongada",
|
||||
"longRunningTooltip": "Quando ativado, o servidor suporta tarefas de longa duração, redefinindo o temporizador de tempo limite ao receber notificações de progresso e estendendo o tempo máximo de tempo limite para 10 minutos.",
|
||||
"marketplaces": "Mercados",
|
||||
"missingDependencies": "Ausente, instale para continuar",
|
||||
"more": {
|
||||
"awesome": "Lista selecionada de servidores MCP",
|
||||
@@ -3839,6 +3880,7 @@
|
||||
"provider": "Fornecedor",
|
||||
"providerPlaceholder": "Nome do Fornecedor",
|
||||
"providerUrl": "URL do Fornecedor",
|
||||
"providers": "Fornecedores",
|
||||
"registry": "Fonte de Gerenciamento de Pacotes",
|
||||
"registryDefault": "Padrão",
|
||||
"registryTooltip": "Selecione uma fonte alternativa para instalar pacotes, caso tenha problemas de rede com a fonte padrão.",
|
||||
@@ -3861,6 +3903,7 @@
|
||||
"searchNpx": "Buscar MCP",
|
||||
"serverPlural": "Servidores",
|
||||
"serverSingular": "Servidor",
|
||||
"servers": "Servidores MCP",
|
||||
"sse": "Eventos do Servidor (sse)",
|
||||
"startError": "Falha ao Iniciar",
|
||||
"stdio": "Entrada/Saída Padrão (stdio)",
|
||||
@@ -4260,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "ID da chave de acesso da AWS",
|
||||
"access_key_id_help": "O seu ID da chave de acesso AWS, utilizado para aceder ao serviço AWS Bedrock",
|
||||
"api_key": "Chave de API do Bedrock",
|
||||
"api_key_help": "Sua Chave de API AWS Bedrock para autenticação",
|
||||
"auth_type": "Tipo de Autenticação",
|
||||
"auth_type_api_key": "Chave de API do Bedrock",
|
||||
"auth_type_help": "Escolha entre credenciais IAM ou autenticação por chave de API do Bedrock",
|
||||
"auth_type_iam": "Credenciais IAM",
|
||||
"description": "A AWS Bedrock é um serviço de modelos fundamentais totalmente gerido fornecido pela Amazon, que suporta diversos modelos avançados de linguagem.",
|
||||
"region": "Região da AWS",
|
||||
"region_help": "A sua região de serviço da AWS, por exemplo, us-east-1",
|
||||
|
||||
@@ -339,6 +339,41 @@
|
||||
},
|
||||
"title": "API Сервер"
|
||||
},
|
||||
"appMenu": {
|
||||
"about": "О",
|
||||
"close": "Закрыть окно",
|
||||
"copy": "Копировать",
|
||||
"cut": "Резать",
|
||||
"delete": "Удалить",
|
||||
"documentation": "Документация",
|
||||
"edit": "Редактировать",
|
||||
"feedback": "Обратная связь",
|
||||
"file": "Файл",
|
||||
"forceReload": "Принудительная перезагрузка",
|
||||
"front": "Показать все поверх других",
|
||||
"help": "Помощь",
|
||||
"hide": "Скрыть",
|
||||
"hideOthers": "Скрыть остальные",
|
||||
"minimize": "Минимизировать",
|
||||
"paste": "Вставить",
|
||||
"quit": "Выйти",
|
||||
"redo": "Переделать",
|
||||
"releases": "Релизы",
|
||||
"reload": "Перезагрузка",
|
||||
"resetZoom": "Настоящий размер",
|
||||
"selectAll": "Выбрать все",
|
||||
"services": "Услуги",
|
||||
"toggleDevTools": "Переключить инструменты разработчика",
|
||||
"toggleFullscreen": "Переключить полноэкранный режим",
|
||||
"undo": "Отменить",
|
||||
"unhide": "Показать все",
|
||||
"view": "Вид",
|
||||
"website": "Веб-сайт",
|
||||
"window": "Окно",
|
||||
"zoom": "Zoom",
|
||||
"zoomIn": "Увеличить",
|
||||
"zoomOut": "Отдалить"
|
||||
},
|
||||
"assistants": {
|
||||
"abbr": "Ассистент",
|
||||
"clear": {
|
||||
@@ -3766,6 +3801,7 @@
|
||||
"description": "Не включать функциональность сервера MCP",
|
||||
"label": "Отключить сервер MCP"
|
||||
},
|
||||
"discover": "Откройте",
|
||||
"duplicateName": "Сервер с таким именем уже существует",
|
||||
"editJson": "Редактировать JSON",
|
||||
"editMcpJson": "Редактировать MCP",
|
||||
@@ -3776,6 +3812,10 @@
|
||||
"32000": "MCP сервер не запущен, пожалуйста, проверьте параметры",
|
||||
"toolNotFound": "Инструмент {{name}} не найден"
|
||||
},
|
||||
"fetch": {
|
||||
"button": "Получить серверы",
|
||||
"success": "Успешно получены MCP-серверы"
|
||||
},
|
||||
"findMore": "Найти больше MCP",
|
||||
"headers": "Заголовки",
|
||||
"headersTooltip": "Пользовательские заголовки для HTTP-запросов",
|
||||
@@ -3791,6 +3831,7 @@
|
||||
"logoUrl": "URL логотипа",
|
||||
"longRunning": "Длительный режим работы",
|
||||
"longRunningTooltip": "Включив эту опцию, сервер будет поддерживать длительные задачи. При получении уведомлений о ходе выполнения будет сброшен тайм-аут и максимальное время выполнения будет увеличено до 10 минут.",
|
||||
"marketplaces": "Торговые площадки",
|
||||
"missingDependencies": "отсутствует, пожалуйста, установите для продолжения.",
|
||||
"more": {
|
||||
"awesome": "Кураторский список серверов MCP",
|
||||
@@ -3839,6 +3880,7 @@
|
||||
"provider": "Провайдер",
|
||||
"providerPlaceholder": "Имя провайдера",
|
||||
"providerUrl": "URL провайдера",
|
||||
"providers": "Поставщики",
|
||||
"registry": "Реестр пакетов",
|
||||
"registryDefault": "По умолчанию",
|
||||
"registryTooltip": "Выберите реестр для установки пакетов, если возникают проблемы с сетью при использовании реестра по умолчанию.",
|
||||
@@ -3861,6 +3903,7 @@
|
||||
"searchNpx": "Найти MCP",
|
||||
"serverPlural": "серверы",
|
||||
"serverSingular": "сервер",
|
||||
"servers": "Серверы MCP",
|
||||
"sse": "События, отправляемые сервером (sse)",
|
||||
"startError": "Запуск не удалось",
|
||||
"stdio": "Стандартный ввод/вывод (stdio)",
|
||||
@@ -4260,6 +4303,12 @@
|
||||
"aws-bedrock": {
|
||||
"access_key_id": "AWS Ключ доступа ID",
|
||||
"access_key_id_help": "Ваш AWS Ключ доступа ID для доступа к AWS Bedrock",
|
||||
"api_key": "Ключ API Bedrock",
|
||||
"api_key_help": "Ваш ключ API AWS Bedrock для аутентификации",
|
||||
"auth_type": "Тип аутентификации",
|
||||
"auth_type_api_key": "Ключ API Bedrock",
|
||||
"auth_type_help": "Выберите между аутентификацией с помощью учетных данных IAM или ключа API Bedrock",
|
||||
"auth_type_iam": "Учетные данные IAM",
|
||||
"description": "AWS Bedrock — это полное управляемое сервисное предложение для моделей, поддерживающее различные современные модели языка",
|
||||
"region": "AWS регион",
|
||||
"region_help": "Ваш регион AWS, например us-east-1",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Alert } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { ContentSearchRef } from '@renderer/components/ContentSearch'
|
||||
import { ContentSearch } from '@renderer/components/ContentSearch'
|
||||
@@ -17,7 +16,7 @@ import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { Flex } from 'antd'
|
||||
import { Alert, Flex } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
import { AnimatePresence, motion } from 'motion/react'
|
||||
import type { FC } from 'react'
|
||||
@@ -170,11 +169,7 @@ const Chat: FC<Props> = (props) => {
|
||||
return () => <div> Active Session ID is invalid.</div>
|
||||
}
|
||||
if (!apiServer.enabled) {
|
||||
return () => (
|
||||
<div>
|
||||
<Alert color="warning" title={t('agent.warning.enable_server')} />
|
||||
</div>
|
||||
)
|
||||
return () => <Alert type="warning" message={t('agent.warning.enable_server')} style={{ margin: '5px 16px' }} />
|
||||
}
|
||||
return () => <AgentSessionMessages agentId={activeAgentId} sessionId={activeSessionId} />
|
||||
}, [activeAgentId, activeSessionId, apiServer.enabled, t])
|
||||
@@ -191,22 +186,14 @@ const Chat: FC<Props> = (props) => {
|
||||
|
||||
// TODO: more info
|
||||
const AgentInvalid = useCallback(() => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div>
|
||||
<Alert color="warning" title="Select an agent" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return <Alert type="warning" message="Select an agent" style={{ margin: '5px 16px' }} />
|
||||
}, [])
|
||||
|
||||
// TODO: more info
|
||||
const SessionInvalid = useCallback(() => {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div>
|
||||
<Alert color="warning" title="Create a session" />
|
||||
</div>
|
||||
<Alert type="warning" message="Create a session" style={{ margin: '5px 16px' }} />
|
||||
</div>
|
||||
)
|
||||
}, [])
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Tooltip } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { ActionIconButton } from '@renderer/components/Buttons'
|
||||
import { QuickPanelView } from '@renderer/components/QuickPanel'
|
||||
@@ -11,6 +10,7 @@ import { useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import PasteService from '@renderer/services/PasteService'
|
||||
import { pauseTrace } from '@renderer/services/SpanManagerService'
|
||||
import { estimateUserPromptUsage } from '@renderer/services/TokenService'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||
import { sendMessage as dispatchSendMessage } from '@renderer/store/thunk/messageThunk'
|
||||
@@ -22,6 +22,7 @@ import { abortCompletion } from '@renderer/utils/abortController'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { getSendMessageShortcutLabel, isSendMessageKeyPressed } from '@renderer/utils/input'
|
||||
import { createMainTextBlock, createMessage } from '@renderer/utils/messageUtils/create'
|
||||
import { Tooltip } from 'antd'
|
||||
import type { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { isEmpty } from 'lodash'
|
||||
@@ -199,11 +200,15 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
}
|
||||
: undefined
|
||||
|
||||
// Calculate token usage for the user message
|
||||
const usage = await estimateUserPromptUsage({ content: text })
|
||||
|
||||
const userMessage: Message = createMessage('user', sessionTopicId, agentId, {
|
||||
id: userMessageId,
|
||||
blocks: userMessageBlocks.map((block) => block?.id),
|
||||
model,
|
||||
modelId: model?.id
|
||||
modelId: model?.id,
|
||||
usage
|
||||
})
|
||||
|
||||
const assistantStub: Assistant = {
|
||||
@@ -309,7 +314,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarGroup>
|
||||
<Tooltip placement="top" content={t('chat.input.new_topic', { Command: newTopicShortcut })} delay={0}>
|
||||
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })}>
|
||||
<ActionIconButton
|
||||
onClick={handleCreateSession}
|
||||
disabled={createSessionDisabled}
|
||||
@@ -321,7 +326,7 @@ const AgentSessionInputbar: FC<Props> = ({ agentId, sessionId }) => {
|
||||
<ToolbarGroup>
|
||||
<SendMessageButton sendMessage={sendMessage} disabled={sendDisabled} />
|
||||
{canAbort && (
|
||||
<Tooltip placement="top" content={t('chat.input.pause')}>
|
||||
<Tooltip placement="top" title={t('chat.input.pause')}>
|
||||
<ActionIconButton onClick={abortAgentSession} style={{ marginRight: -2 }}>
|
||||
<CirclePause size={20} color="var(--color-error)" />
|
||||
</ActionIconButton>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Button } from '@heroui/button'
|
||||
import CodeViewer from '@renderer/components/CodeViewer'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
} from '@renderer/types/error'
|
||||
import type { ErrorMessageBlock, Message } from '@renderer/types/newMessage'
|
||||
import { formatAiSdkError, formatError, safeToString } from '@renderer/utils/error'
|
||||
import { Button } from 'antd'
|
||||
import { Alert as AntdAlert, Modal } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
@@ -144,9 +144,11 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock; message: Message }>
|
||||
onClick={showErrorDetail}
|
||||
style={{ cursor: 'pointer' }}
|
||||
action={
|
||||
<Button size="sm" className="p-0" variant="light" onPress={showErrorDetail}>
|
||||
{t('common.detail')}
|
||||
</Button>
|
||||
<>
|
||||
<Button size="middle" color="default" variant="text" onClick={showErrorDetail}>
|
||||
{t('common.detail')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ErrorDetailModal open={showDetailModal} onClose={() => setShowDetailModal(false)} error={block.error} />
|
||||
@@ -198,10 +200,10 @@ const ErrorDetailModal: React.FC<ErrorDetailModalProps> = ({ open, onClose, erro
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="copy" size="sm" variant="light" onPress={copyErrorDetails}>
|
||||
<Button key="copy" variant="text" color="default" onClick={copyErrorDetails}>
|
||||
{t('common.copy')}
|
||||
</Button>,
|
||||
<Button key="close" size="sm" variant="light" onPress={onClose}>
|
||||
<Button key="close" variant="text" color={'default'} onClick={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Spinner } from '@heroui/react'
|
||||
import { MessageBlockStatus, MessageBlockType, type PlaceholderMessageBlock } from '@renderer/types/newMessage'
|
||||
import React from 'react'
|
||||
import { BeatLoader } from 'react-spinners'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface PlaceholderBlockProps {
|
||||
@@ -10,7 +10,7 @@ const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ block }) => {
|
||||
if (block.status === MessageBlockStatus.PROCESSING && block.type === MessageBlockType.UNKNOWN) {
|
||||
return (
|
||||
<MessageContentLoading>
|
||||
<Spinner color="current" variant="dots" />
|
||||
<BeatLoader color="var(--color-text-1)" size={8} speedMultiplier={0.8} />
|
||||
</MessageContentLoading>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
@@ -15,7 +14,7 @@ import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { estimateMessageUsage } from '@renderer/services/TokenService'
|
||||
import type { Assistant, Topic } from '@renderer/types'
|
||||
import type { Message, MessageBlock } from '@renderer/types/newMessage'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { classNames, cn } from '@renderer/utils'
|
||||
import { isMessageProcessing } from '@renderer/utils/messageUtils/is'
|
||||
import { Divider } from 'antd'
|
||||
import type { Dispatch, FC, SetStateAction } from 'react'
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AccordionItem, Chip, Code } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Tag } from 'antd'
|
||||
import { CheckCircle, Terminal, XCircle } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
@@ -15,7 +16,13 @@ interface ParsedBashOutput {
|
||||
tool_use_error?: string
|
||||
}
|
||||
|
||||
export function BashOutputTool({ input, output }: { input: BashOutputToolInput; output?: BashOutputToolOutput }) {
|
||||
export function BashOutputTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: BashOutputToolInput
|
||||
output?: BashOutputToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 解析 XML 输出
|
||||
const parsedOutput = useMemo(() => {
|
||||
if (!output) return null
|
||||
@@ -84,93 +91,88 @@ export function BashOutputTool({ input, output }: { input: BashOutputToolInput;
|
||||
} as const
|
||||
}, [parsedOutput])
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.BashOutput}
|
||||
aria-label="BashOutput Tool"
|
||||
title={
|
||||
const children = parsedOutput ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Status Info */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{parsedOutput.exit_code !== undefined && (
|
||||
<Tag color={parsedOutput.exit_code === 0 ? 'success' : 'danger'}>Exit Code: {parsedOutput.exit_code}</Tag>
|
||||
)}
|
||||
{parsedOutput.timestamp && (
|
||||
<Tag className="py-0 font-mono text-xs">{new Date(parsedOutput.timestamp).toLocaleString()}</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Standard Output */}
|
||||
{parsedOutput.stdout && (
|
||||
<div>
|
||||
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
||||
{parsedOutput.stdout}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Standard Error */}
|
||||
{parsedOutput.stderr && (
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.stderr}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Use Error */}
|
||||
{parsedOutput.tool_use_error && (
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<XCircle className="h-4 w-4 text-danger" />
|
||||
<span className="font-medium text-danger-600 text-xs">Error:</span>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.tool_use_error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 原始输出(如果解析失败或非 XML 格式)
|
||||
output && (
|
||||
<div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">{output}</pre>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
return {
|
||||
key: AgentToolsType.BashOutput,
|
||||
label: (
|
||||
<>
|
||||
<ToolTitle
|
||||
icon={<Terminal className="h-4 w-4" />}
|
||||
label="Bash Output"
|
||||
params={
|
||||
<div className="flex items-center gap-2">
|
||||
<Code size="sm" className="py-0 text-xs">
|
||||
{input.bash_id}
|
||||
</Code>
|
||||
<Tag className="py-0 font-mono text-xs">{input.bash_id}</Tag>
|
||||
{statusConfig && (
|
||||
<Chip
|
||||
size="sm"
|
||||
<Tag
|
||||
color={statusConfig.color}
|
||||
variant="flat"
|
||||
startContent={statusConfig.icon}
|
||||
className="h-5">
|
||||
icon={statusConfig.icon}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: '2px'
|
||||
}}>
|
||||
{statusConfig.text}
|
||||
</Chip>
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
classNames={{
|
||||
content: 'space-y-3 px-1'
|
||||
}}>
|
||||
{parsedOutput ? (
|
||||
<>
|
||||
{/* Status Info */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{parsedOutput.exit_code !== undefined && (
|
||||
<Chip size="sm" color={parsedOutput.exit_code === 0 ? 'success' : 'danger'} variant="flat">
|
||||
Exit Code: {parsedOutput.exit_code}
|
||||
</Chip>
|
||||
)}
|
||||
{parsedOutput.timestamp && (
|
||||
<Code size="sm" className="py-0 text-xs">
|
||||
{new Date(parsedOutput.timestamp).toLocaleString()}
|
||||
</Code>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
|
||||
{/* Standard Output */}
|
||||
{parsedOutput.stdout && (
|
||||
<div>
|
||||
<div className="mb-2 font-medium text-default-600 text-xs">stdout:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">
|
||||
{parsedOutput.stdout}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Standard Error */}
|
||||
{parsedOutput.stderr && (
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 font-medium text-danger-600 text-xs">stderr:</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.stderr}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Use Error */}
|
||||
{parsedOutput.tool_use_error && (
|
||||
<div className="border border-danger-200">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<XCircle className="h-4 w-4 text-danger" />
|
||||
<span className="font-medium text-danger-600 text-xs">Error:</span>
|
||||
</div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-danger-600 text-xs dark:text-danger-400">
|
||||
{parsedOutput.tool_use_error}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 原始输出(如果解析失败或非 XML 格式)
|
||||
output && (
|
||||
<div>
|
||||
<pre className="whitespace-pre-wrap font-mono text-default-700 text-xs dark:text-default-300">{output}</pre>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</AccordionItem>
|
||||
)
|
||||
children: children
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import { AccordionItem, Code } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Tag } from 'antd'
|
||||
import { Terminal } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { BashToolInput as BashToolInputType, BashToolOutput as BashToolOutputType } from './types'
|
||||
|
||||
export function BashTool({ input, output }: { input: BashToolInputType; output?: BashToolOutputType }) {
|
||||
export function BashTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: BashToolInputType
|
||||
output?: BashToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算输出行数
|
||||
const outputLines = output ? output.split('\n').length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Bash Tool"
|
||||
title={
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<>
|
||||
<ToolTitle
|
||||
icon={<Terminal className="h-4 w-4" />}
|
||||
label="Bash"
|
||||
params={input.description}
|
||||
stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
}
|
||||
subtitle={
|
||||
<Code size="sm" className="line-clamp-1 w-max max-w-full text-ellipsis py-0 text-xs">
|
||||
{input.command}
|
||||
</Code>
|
||||
}>
|
||||
<div className="whitespace-pre-line">{output}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
<div className="mt-1">
|
||||
<Tag className="whitespace-pre-wrap break-all font-mono">{input.command}</Tag>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
children: <div className="whitespace-pre-line">{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileEdit } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
@@ -28,19 +28,26 @@ export const renderCodeBlock = (content: string, variant: 'old' | 'new') => {
|
||||
)
|
||||
}
|
||||
|
||||
export function EditTool({ input, output }: { input: EditToolInput; output?: EditToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.Edit}
|
||||
aria-label="Edit Tool"
|
||||
title={<ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input.file_path} />}>
|
||||
{/* Diff View */}
|
||||
{/* Old Content */}
|
||||
{renderCodeBlock(input.old_string, 'old')}
|
||||
{/* New Content */}
|
||||
{renderCodeBlock(input.new_string, 'new')}
|
||||
{/* Output */}
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function EditTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: EditToolInput
|
||||
output?: EditToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: AgentToolsType.Edit,
|
||||
label: <ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input.file_path} />,
|
||||
children: (
|
||||
<>
|
||||
{/* Diff View */}
|
||||
{/* Old Content */}
|
||||
{renderCodeBlock(input.old_string, 'old')}
|
||||
{/* New Content */}
|
||||
{renderCodeBlock(input.new_string, 'new')}
|
||||
{/* Output */}
|
||||
{output}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { DoorOpen } from 'lucide-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
@@ -6,19 +6,22 @@ import { ToolTitle } from './GenericTools'
|
||||
import type { ExitPlanModeToolInput, ExitPlanModeToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function ExitPlanModeTool({ input, output }: { input: ExitPlanModeToolInput; output?: ExitPlanModeToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.ExitPlanMode}
|
||||
aria-label="ExitPlanMode Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<DoorOpen className="h-4 w-4" />}
|
||||
label="ExitPlanMode"
|
||||
stats={`${input.plan.split('\n\n').length} plans`}
|
||||
/>
|
||||
}>
|
||||
{<ReactMarkdown>{input.plan + '\n\n' + (output ?? '')}</ReactMarkdown>}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function ExitPlanModeTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: ExitPlanModeToolInput
|
||||
output?: ExitPlanModeToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: AgentToolsType.ExitPlanMode,
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<DoorOpen className="h-4 w-4" />}
|
||||
label="ExitPlanMode"
|
||||
stats={`${input.plan.split('\n\n').length} plans`}
|
||||
/>
|
||||
),
|
||||
children: <ReactMarkdown>{input.plan + '\n\n' + (output ?? '')}</ReactMarkdown>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FolderSearch } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { GlobToolInput as GlobToolInputType, GlobToolOutput as GlobToolOutputType } from './types'
|
||||
|
||||
export function GlobTool({ input, output }: { input: GlobToolInputType; output?: GlobToolOutputType }) {
|
||||
export function GlobTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: GlobToolInputType
|
||||
output?: GlobToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算文件数量
|
||||
const lineCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Glob Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<FolderSearch className="h-4 w-4" />}
|
||||
label="Glob"
|
||||
params={input.pattern}
|
||||
stats={output ? `${lineCount} of output` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>{output}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<FolderSearch className="h-4 w-4" />}
|
||||
label="Glob"
|
||||
params={input.pattern}
|
||||
stats={output ? `${lineCount} ${lineCount === 1 ? 'file' : 'files'}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileSearch } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { GrepToolInput, GrepToolOutput } from './types'
|
||||
|
||||
export function GrepTool({ input, output }: { input: GrepToolInput; output?: GrepToolOutput }) {
|
||||
export function GrepTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: GrepToolInput
|
||||
output?: GrepToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算结果行数
|
||||
const resultLines = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Grep Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<FileSearch className="h-4 w-4" />}
|
||||
label="Grep"
|
||||
params={
|
||||
<>
|
||||
{input.pattern}
|
||||
{input.output_mode && <span className="ml-1">({input.output_mode})</span>}
|
||||
</>
|
||||
}
|
||||
stats={output ? `${resultLines} ${resultLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
}>
|
||||
<div>{output}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<FileSearch className="h-4 w-4" />}
|
||||
label="Grep"
|
||||
params={
|
||||
<>
|
||||
{input.pattern}
|
||||
{input.output_mode && <span className="ml-1">({input.output_mode})</span>}
|
||||
</>
|
||||
}
|
||||
stats={output ? `${resultLines} ${resultLines === 1 ? 'line' : 'lines'}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileText } from 'lucide-react'
|
||||
|
||||
import { renderCodeBlock } from './EditTool'
|
||||
@@ -6,18 +6,24 @@ import { ToolTitle } from './GenericTools'
|
||||
import type { MultiEditToolInput, MultiEditToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function MultiEditTool({ input }: { input: MultiEditToolInput; output?: MultiEditToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.MultiEdit}
|
||||
aria-label="MultiEdit Tool"
|
||||
title={<ToolTitle icon={<FileText className="h-4 w-4" />} label="MultiEdit" params={input.file_path} />}>
|
||||
{input.edits.map((edit, index) => (
|
||||
<div key={index}>
|
||||
{renderCodeBlock(edit.old_string, 'old')}
|
||||
{renderCodeBlock(edit.new_string, 'new')}
|
||||
</div>
|
||||
))}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function MultiEditTool({
|
||||
input
|
||||
}: {
|
||||
input: MultiEditToolInput
|
||||
output?: MultiEditToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: AgentToolsType.MultiEdit,
|
||||
label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="MultiEdit" params={input.file_path} />,
|
||||
children: (
|
||||
<div>
|
||||
{input.edits.map((edit, index) => (
|
||||
<div key={index}>
|
||||
{renderCodeBlock(edit.old_string, 'old')}
|
||||
{renderCodeBlock(edit.new_string, 'new')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Tag } from 'antd'
|
||||
import { FileText } from 'lucide-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
@@ -6,14 +7,23 @@ import { ToolTitle } from './GenericTools'
|
||||
import type { NotebookEditToolInput, NotebookEditToolOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function NotebookEditTool({ input, output }: { input: NotebookEditToolInput; output?: NotebookEditToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.NotebookEdit}
|
||||
aria-label="NotebookEdit Tool"
|
||||
title={<ToolTitle icon={<FileText className="h-4 w-4" />} label="NotebookEdit" />}
|
||||
subtitle={input.notebook_path}>
|
||||
<ReactMarkdown>{output}</ReactMarkdown>
|
||||
</AccordionItem>
|
||||
)
|
||||
export function NotebookEditTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: NotebookEditToolInput
|
||||
output?: NotebookEditToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: AgentToolsType.NotebookEdit,
|
||||
label: (
|
||||
<>
|
||||
<ToolTitle icon={<FileText className="h-4 w-4" />} label="NotebookEdit" />
|
||||
<Tag className="mt-1" color="blue">
|
||||
{input.notebook_path}{' '}
|
||||
</Tag>
|
||||
</>
|
||||
),
|
||||
children: <ReactMarkdown>{output}</ReactMarkdown>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileText } from 'lucide-react'
|
||||
import { useMemo } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
@@ -7,7 +7,13 @@ import { ToolTitle } from './GenericTools'
|
||||
import type { ReadToolInput as ReadToolInputType, ReadToolOutput as ReadToolOutputType, TextOutput } from './types'
|
||||
import { AgentToolsType } from './types'
|
||||
|
||||
export function ReadTool({ input, output }: { input: ReadToolInputType; output?: ReadToolOutputType }) {
|
||||
export function ReadTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: ReadToolInputType
|
||||
output?: ReadToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 移除 system-reminder 标签及其内容的辅助函数
|
||||
const removeSystemReminderTags = (text: string): string => {
|
||||
// 使用正则表达式匹配 <system-reminder> 标签及其内容,包括换行符
|
||||
@@ -53,19 +59,16 @@ export function ReadTool({ input, output }: { input: ReadToolInputType; output?:
|
||||
}
|
||||
}, [outputString])
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.Read}
|
||||
aria-label="Read Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<FileText className="h-4 w-4" />}
|
||||
label="Read File"
|
||||
params={input.file_path.split('/').pop()}
|
||||
stats={stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined}
|
||||
/>
|
||||
}>
|
||||
{outputString ? <ReactMarkdown>{outputString}</ReactMarkdown> : null}
|
||||
</AccordionItem>
|
||||
)
|
||||
return {
|
||||
key: AgentToolsType.Read,
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<FileText className="h-4 w-4" />}
|
||||
label="Read File"
|
||||
params={input.file_path.split('/').pop()}
|
||||
stats={stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: outputString ? <ReactMarkdown>{outputString}</ReactMarkdown> : null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Search } from 'lucide-react'
|
||||
|
||||
import { StringInputTool, StringOutputTool, ToolTitle } from './GenericTools'
|
||||
import type { SearchToolInput as SearchToolInputType, SearchToolOutput as SearchToolOutputType } from './types'
|
||||
|
||||
export function SearchTool({ input, output }: { input: SearchToolInputType; output?: SearchToolOutputType }) {
|
||||
export function SearchTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: SearchToolInputType
|
||||
output?: SearchToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算结果数量
|
||||
const resultCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Search Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<Search className="h-4 w-4" />}
|
||||
label="Search"
|
||||
params={`"${input}"`}
|
||||
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
|
||||
/>
|
||||
}>
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<Search className="h-4 w-4" />}
|
||||
label="Search"
|
||||
params={`"${input}"`}
|
||||
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: (
|
||||
<div>
|
||||
<StringInputTool input={input} label="Search Query" />
|
||||
{output && (
|
||||
@@ -28,6 +33,6 @@ export function SearchTool({ input, output }: { input: SearchToolInputType; outp
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { PencilRuler } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { SkillToolInput, SkillToolOutput } from './types'
|
||||
|
||||
export function SkillTool({ input, output }: { input: SkillToolInput; output?: SkillToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Skill Tool"
|
||||
title={<ToolTitle icon={<PencilRuler className="h-4 w-4" />} label="Skill" params={input.command} />}>
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function SkillTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: SkillToolInput
|
||||
output?: SkillToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: 'tool',
|
||||
label: <ToolTitle icon={<PencilRuler className="h-4 w-4" />} label="Skill" params={input.command} />,
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Bot } from 'lucide-react'
|
||||
import Markdown from 'react-markdown'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { TaskToolInput as TaskToolInputType, TaskToolOutput as TaskToolOutputType } from './types'
|
||||
|
||||
export function TaskTool({ input, output }: { input: TaskToolInputType; output?: TaskToolOutputType }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Task Tool"
|
||||
title={<ToolTitle icon={<Bot className="h-4 w-4" />} label="Task" params={input.description} />}>
|
||||
{output?.map((item) => (
|
||||
<div key={item.type}>
|
||||
<div>{item.type === 'text' ? <Markdown>{item.text}</Markdown> : item.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function TaskTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: TaskToolInputType
|
||||
output?: TaskToolOutputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: 'tool',
|
||||
label: <ToolTitle icon={<Bot className="h-4 w-4" />} label="Task" params={input.description} />,
|
||||
children: (
|
||||
<div>
|
||||
{output?.map((item) => (
|
||||
<div key={item.type}>
|
||||
<div>{item.type === 'text' ? <Markdown>{item.text}</Markdown> : item.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { AccordionItem, Card, CardBody, Chip } from '@heroui/react'
|
||||
import { cn } from '@renderer/utils'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Card } from 'antd'
|
||||
import { CheckCircle, Circle, Clock, ListTodo } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
@@ -30,44 +32,59 @@ const getStatusConfig = (status: TodoItem['status']) => {
|
||||
}
|
||||
}
|
||||
|
||||
export function TodoWriteTool({ input }: { input: TodoWriteToolInputType }) {
|
||||
export function TodoWriteTool({
|
||||
input
|
||||
}: {
|
||||
input: TodoWriteToolInputType
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
const doneCount = input.todos.filter((todo) => todo.status === 'completed').length
|
||||
return (
|
||||
<AccordionItem
|
||||
key={AgentToolsType.TodoWrite}
|
||||
aria-label="Todo Write Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<ListTodo className="h-4 w-4" />}
|
||||
label="Todo Write"
|
||||
params={`${doneCount} Done`}
|
||||
stats={`${input.todos.length} ${input.todos.length === 1 ? 'item' : 'items'}`}
|
||||
/>
|
||||
}>
|
||||
|
||||
return {
|
||||
key: AgentToolsType.TodoWrite,
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<ListTodo className="h-4 w-4" />}
|
||||
label="Todo Write"
|
||||
params={`${doneCount} Done`}
|
||||
stats={`${input.todos.length} ${input.todos.length === 1 ? 'item' : 'items'}`}
|
||||
/>
|
||||
),
|
||||
children: (
|
||||
<div className="space-y-3">
|
||||
{input.todos.map((todo, index) => {
|
||||
const statusConfig = getStatusConfig(todo.status)
|
||||
return (
|
||||
<Card key={index} className="shadow-sm">
|
||||
<CardBody className="p-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<Chip color={statusConfig.color} variant="flat" size="sm" className="flex-shrink-0">
|
||||
{statusConfig.icon}
|
||||
</Chip>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className={`text-sm ${todo.status === 'completed' ? 'text-default-500 line-through' : ''}`}>
|
||||
{todo.status === 'completed' ? <s>{todo.content}</s> : todo.content}
|
||||
<div key={index}>
|
||||
<Card
|
||||
key={index}
|
||||
className="shadow-sm"
|
||||
styles={{
|
||||
body: { padding: 2 }
|
||||
}}>
|
||||
<div className="p-2">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center rounded-full border bg-opacity-50 p-2',
|
||||
`bg-${statusConfig.color}`
|
||||
)}>
|
||||
{statusConfig.icon}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className={`text-sm ${todo.status === 'completed' ? 'text-default-500 line-through' : ''}`}>
|
||||
{todo.status === 'completed' ? <s>{todo.content}</s> : todo.content}
|
||||
</div>
|
||||
{todo.status === 'in_progress' && (
|
||||
<div className="mt-1 text-default-400 text-xs">{todo.activeForm}</div>
|
||||
)}
|
||||
</div>
|
||||
{todo.status === 'in_progress' && (
|
||||
<div className="mt-1 text-default-400 text-xs">{todo.activeForm}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Wrench } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
@@ -11,7 +11,11 @@ interface UnknownToolProps {
|
||||
output?: unknown
|
||||
}
|
||||
|
||||
export function UnknownToolRenderer({ toolName = '', input, output }: UnknownToolProps) {
|
||||
export function UnknownToolRenderer({
|
||||
toolName = '',
|
||||
input,
|
||||
output
|
||||
}: UnknownToolProps): NonNullable<CollapseProps['items']>[number] {
|
||||
const { highlightCode } = useCodeStyle()
|
||||
const [inputHtml, setInputHtml] = useState<string>('')
|
||||
const [outputHtml, setOutputHtml] = useState<string>('')
|
||||
@@ -47,17 +51,16 @@ export function UnknownToolRenderer({ toolName = '', input, output }: UnknownToo
|
||||
return 'Tool'
|
||||
}
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="unknown-tool"
|
||||
aria-label={toolName}
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<Wrench className="h-4 w-4" />}
|
||||
label={getToolDisplayName(toolName)}
|
||||
params={getToolDescription()}
|
||||
/>
|
||||
}>
|
||||
return {
|
||||
key: 'unknown-tool',
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<Wrench className="h-4 w-4" />}
|
||||
label={getToolDisplayName(toolName)}
|
||||
params={getToolDescription()}
|
||||
/>
|
||||
),
|
||||
children: (
|
||||
<div className="space-y-3">
|
||||
{input !== undefined && (
|
||||
<div>
|
||||
@@ -83,6 +86,6 @@ export function UnknownToolRenderer({ toolName = '', input, output }: UnknownToo
|
||||
<div className="text-foreground-500 text-xs">No data available for this tool</div>
|
||||
)}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Globe } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { WebFetchToolInput, WebFetchToolOutput } from './types'
|
||||
|
||||
export function WebFetchTool({ input, output }: { input: WebFetchToolInput; output?: WebFetchToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Web Fetch Tool"
|
||||
title={<ToolTitle icon={<Globe className="h-4 w-4" />} label="Web Fetch" params={input.url} />}
|
||||
subtitle={input.prompt}>
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
export function WebFetchTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: WebFetchToolInput
|
||||
output?: WebFetchToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: 'tool',
|
||||
label: <ToolTitle icon={<Globe className="h-4 w-4" />} label="Web Fetch" params={input.url} />,
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Globe } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { WebSearchToolInput, WebSearchToolOutput } from './types'
|
||||
|
||||
export function WebSearchTool({ input, output }: { input: WebSearchToolInput; output?: WebSearchToolOutput }) {
|
||||
export function WebSearchTool({
|
||||
input,
|
||||
output
|
||||
}: {
|
||||
input: WebSearchToolInput
|
||||
output?: WebSearchToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
// 如果有输出,计算结果数量
|
||||
const resultCount = output ? output.split('\n').filter((line) => line.trim()).length : 0
|
||||
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Web Search Tool"
|
||||
title={
|
||||
<ToolTitle
|
||||
icon={<Globe className="h-4 w-4" />}
|
||||
label="Web Search"
|
||||
params={input.query}
|
||||
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
|
||||
/>
|
||||
}>
|
||||
{output}
|
||||
</AccordionItem>
|
||||
)
|
||||
return {
|
||||
key: 'tool',
|
||||
label: (
|
||||
<ToolTitle
|
||||
icon={<Globe className="h-4 w-4" />}
|
||||
label="Web Search"
|
||||
params={input.query}
|
||||
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
|
||||
/>
|
||||
),
|
||||
children: <div>{output}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { AccordionItem } from '@heroui/react'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { FileText } from 'lucide-react'
|
||||
|
||||
import { ToolTitle } from './GenericTools'
|
||||
import type { WriteToolInput, WriteToolOutput } from './types'
|
||||
|
||||
export function WriteTool({ input }: { input: WriteToolInput; output?: WriteToolOutput }) {
|
||||
return (
|
||||
<AccordionItem
|
||||
key="tool"
|
||||
aria-label="Write Tool"
|
||||
title={<ToolTitle icon={<FileText className="h-4 w-4" />} label="Write" params={input.file_path} />}>
|
||||
<div>{input.content}</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
export function WriteTool({
|
||||
input
|
||||
}: {
|
||||
input: WriteToolInput
|
||||
output?: WriteToolOutput
|
||||
}): NonNullable<CollapseProps['items']>[number] {
|
||||
return {
|
||||
key: 'tool',
|
||||
label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="Write" params={input.file_path} />,
|
||||
children: <div>{input.content}</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Accordion } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import type { NormalToolResponse } from '@renderer/types'
|
||||
import type { CollapseProps } from 'antd'
|
||||
import { Collapse } from 'antd'
|
||||
|
||||
// 导出所有类型
|
||||
export * from './types'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
|
||||
// 导入所有渲染器
|
||||
import ToolPermissionRequestCard from '../ToolPermissionRequestCard'
|
||||
import { BashOutputTool } from './BashOutputTool'
|
||||
@@ -58,25 +61,27 @@ export function isValidAgentToolsType(toolName: unknown): toolName is AgentTools
|
||||
function renderToolContent(toolName: AgentToolsType, input: ToolInput, output?: ToolOutput) {
|
||||
const Renderer = toolRenderers[toolName]
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const toolContentItem = useMemo(() => {
|
||||
const rendered = Renderer
|
||||
? Renderer({ input: input as any, output: output as any })
|
||||
: UnknownToolRenderer({ input: input as any, output: output as any, toolName })
|
||||
return {
|
||||
...rendered,
|
||||
classNames: {
|
||||
body: 'bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll'
|
||||
} as NonNullable<CollapseProps['items']>[number]['classNames']
|
||||
} as NonNullable<CollapseProps['items']>[number]
|
||||
}, [Renderer, input, output, toolName])
|
||||
|
||||
return (
|
||||
<div className="w-max max-w-full rounded-md bg-foreground-100 py-1 transition-all duration-300 ease-in-out dark:bg-foreground-100">
|
||||
<Accordion
|
||||
className="w-max max-w-full"
|
||||
itemClasses={{
|
||||
trigger:
|
||||
'p-0 [&>div:first-child]:!flex-none [&>div:first-child]:flex [&>div:first-child]:flex-col [&>div:first-child]:text-start [&>div:first-child]:max-w-full',
|
||||
indicator: 'flex-shrink-0',
|
||||
subtitle: 'text-xs',
|
||||
content:
|
||||
'rounded-md bg-foreground-50 p-2 text-foreground-900 dark:bg-foreground-100 max-h-96 p-2 overflow-scroll',
|
||||
base: 'space-y-1'
|
||||
}}
|
||||
defaultExpandedKeys={toolName === AgentToolsType.TodoWrite ? [AgentToolsType.TodoWrite] : []}>
|
||||
{Renderer
|
||||
? Renderer({ input: input as any, output: output as any })
|
||||
: UnknownToolRenderer({ input: input as any, output: output as any, toolName })}
|
||||
</Accordion>
|
||||
</div>
|
||||
<Collapse
|
||||
className="w-max max-w-full"
|
||||
expandIconPosition="end"
|
||||
size="small"
|
||||
defaultActiveKey={toolName === AgentToolsType.TodoWrite ? [AgentToolsType.TodoWrite] : []}
|
||||
items={[toolContentItem]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'
|
||||
import { Button, Chip, ScrollShadow } from '@heroui/react'
|
||||
import { loggerService } from '@logger'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { selectPendingPermissionByToolName, toolPermissionsActions } from '@renderer/store/toolPermissions'
|
||||
import type { NormalToolResponse } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
import { ChevronDown, CirclePlay, CircleX } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -127,33 +127,39 @@ export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<Chip color={isExpired ? 'danger' : 'warning'} size="sm" variant="flat">
|
||||
<div
|
||||
className={`rounded px-2 py-0.5 font-medium text-xs ${
|
||||
isExpired ? 'text-[var(--color-error)]' : 'text-[var(--color-status-warning)]'
|
||||
}`}>
|
||||
{isExpired
|
||||
? t('agent.toolPermission.expired')
|
||||
: t('agent.toolPermission.pending', { seconds: remainingSeconds })}
|
||||
</Chip>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
aria-label={t('agent.toolPermission.aria.denyRequest')}
|
||||
className="h-8"
|
||||
color="danger"
|
||||
isDisabled={isSubmitting || isExpired}
|
||||
isLoading={isSubmittingDeny}
|
||||
onPress={() => handleDecision('deny')}
|
||||
startContent={<CircleX size={16} />}
|
||||
variant="bordered">
|
||||
disabled={isSubmitting || isExpired}
|
||||
loading={isSubmittingDeny}
|
||||
onClick={() => handleDecision('deny')}
|
||||
icon={<CircleX size={16} />}
|
||||
iconPosition={'start'}
|
||||
variant="outlined">
|
||||
{t('agent.toolPermission.button.cancel')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
aria-label={t('agent.toolPermission.aria.allowRequest')}
|
||||
className="h-8 px-3"
|
||||
color="success"
|
||||
isDisabled={isSubmitting || isExpired}
|
||||
isLoading={isSubmittingAllow}
|
||||
onPress={() => handleDecision('allow')}
|
||||
startContent={<CirclePlay size={16} />}>
|
||||
color="primary"
|
||||
disabled={isSubmitting || isExpired}
|
||||
loading={isSubmittingAllow}
|
||||
onClick={() => handleDecision('allow')}
|
||||
icon={<CirclePlay size={16} />}
|
||||
iconPosition={'start'}
|
||||
variant="solid">
|
||||
{t('agent.toolPermission.button.run')}
|
||||
</Button>
|
||||
|
||||
@@ -161,12 +167,12 @@ export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
||||
aria-label={
|
||||
showDetails ? t('agent.toolPermission.aria.hideDetails') : t('agent.toolPermission.aria.showDetails')
|
||||
}
|
||||
className="h-8"
|
||||
isIconOnly
|
||||
onPress={() => setShowDetails((value) => !value)}
|
||||
variant="light">
|
||||
<ChevronDown className={`transition-transform ${showDetails ? 'rotate-180' : ''}`} size={16} />
|
||||
</Button>
|
||||
className="h-8 text-default-600 transition-colors hover:bg-default-200/50 hover:text-default-800"
|
||||
onClick={() => setShowDetails((value) => !value)}
|
||||
icon={<ChevronDown className={`transition-transform ${showDetails ? 'rotate-180' : ''}`} size={16} />}
|
||||
variant="text"
|
||||
style={{ backgroundColor: 'transparent' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,9 +187,9 @@ export function ToolPermissionRequestCard({ toolResponse }: Props) {
|
||||
<p className="mb-2 font-medium text-default-400 text-xs uppercase tracking-wide">
|
||||
{t('agent.toolPermission.inputPreview')}
|
||||
</p>
|
||||
<ScrollShadow className="max-h-48 font-mono text-xs" hideScrollBar>
|
||||
<pre className="whitespace-pre-wrap break-all text-left">{request.inputPreview}</pre>
|
||||
</ScrollShadow>
|
||||
<div className="max-h-[192px] overflow-auto font-mono text-xs">
|
||||
<pre className="whitespace-pre-wrap break-all p-2 text-left">{request.inputPreview}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{request.requiresPermissions && (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Alert, Spinner } from '@heroui/react'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAgents } from '@renderer/hooks/agents/useAgents'
|
||||
import { useApiServer } from '@renderer/hooks/useApiServer'
|
||||
@@ -7,13 +6,9 @@ import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
|
||||
import { useTags } from '@renderer/hooks/useTags'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { addIknowAction } from '@renderer/store/runtime'
|
||||
import type { Assistant, AssistantsSortType, Topic } from '@renderer/types'
|
||||
import { getErrorMessage } from '@renderer/utils'
|
||||
import type { FC } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import UnifiedAddButton from './components/UnifiedAddButton'
|
||||
@@ -31,16 +26,12 @@ interface AssistantsTabProps {
|
||||
onCreateDefaultAssistant: () => void
|
||||
}
|
||||
|
||||
const ALERT_KEY = 'enable_api_server_to_use_agent'
|
||||
|
||||
const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { t } = useTranslation()
|
||||
const { apiServerConfig, apiServerRunning, apiServerLoading } = useApiServer()
|
||||
const { apiServerConfig } = useApiServer()
|
||||
const apiServerEnabled = apiServerConfig.enabled
|
||||
const { iknow, chat } = useRuntime()
|
||||
const dispatch = useAppDispatch()
|
||||
const { chat } = useRuntime()
|
||||
|
||||
// Agent related hooks
|
||||
const { agents, deleteAgent, isLoading: agentsLoading, error: agentsError } = useAgents()
|
||||
@@ -126,31 +117,6 @@ const AssistantsTab: FC<AssistantsTabProps> = (props) => {
|
||||
|
||||
return (
|
||||
<Container className="assistants-tab" ref={containerRef}>
|
||||
{!apiServerConfig.enabled && !apiServerRunning && !iknow[ALERT_KEY] && (
|
||||
<Alert
|
||||
color="warning"
|
||||
title={t('agent.warning.enable_server')}
|
||||
isClosable
|
||||
onClose={() => {
|
||||
dispatch(addIknowAction(ALERT_KEY))
|
||||
}}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{(agentsLoading || apiServerLoading) && <Spinner />}
|
||||
{apiServerConfig.enabled && !apiServerLoading && !apiServerRunning && (
|
||||
<Alert color="danger" title={t('agent.server.error.not_running')} isClosable className="mb-2" />
|
||||
)}
|
||||
{apiServerRunning && agentsError && (
|
||||
<Alert
|
||||
color="danger"
|
||||
title={t('agent.list.error.failed')}
|
||||
description={getErrorMessage(agentsError)}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
<UnifiedAddButton
|
||||
onCreateAssistant={onCreateAssistant}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Button, Divider } from '@heroui/react'
|
||||
import type { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
|
||||
import { SettingDivider } from '@renderer/pages/settings'
|
||||
import { SessionSettingsPopup } from '@renderer/pages/settings/AgentSettings'
|
||||
import AdvancedSettings from '@renderer/pages/settings/AgentSettings/AdvancedSettings'
|
||||
import EssentialSettings from '@renderer/pages/settings/AgentSettings/EssentialSettings'
|
||||
import type { GetAgentSessionResponse } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -31,9 +32,10 @@ const SessionSettingsTab: FC<Props> = ({ session, update }) => {
|
||||
return (
|
||||
<div className="w-[var(--assistants-width)] p-2 px-3 pt-4">
|
||||
<EssentialSettings agentBase={session} update={update} showModelSetting={false} />
|
||||
<SettingDivider />
|
||||
<AdvancedSettings agentBase={session} update={update} />
|
||||
<Divider className="my-2" />
|
||||
<Button size="sm" fullWidth onPress={onMoreSetting}>
|
||||
<SettingDivider />
|
||||
<Button size="small" block onClick={onMoreSetting}>
|
||||
{t('settings.moresetting.label')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Alert, cn } from '@heroui/react'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { cn } from '@renderer/utils'
|
||||
import { Alert } from 'antd'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
@@ -17,19 +18,11 @@ const SessionsTab: FC<SessionsTabProps> = () => {
|
||||
const { apiServer } = useSettings()
|
||||
|
||||
if (!apiServer.enabled) {
|
||||
return (
|
||||
<div>
|
||||
<Alert color="warning" title={t('agent.warning.enable_server')} />
|
||||
</div>
|
||||
)
|
||||
return <Alert type="warning" message={t('agent.warning.enable_server')} style={{ margin: 10 }} />
|
||||
}
|
||||
|
||||
if (!activeAgentId) {
|
||||
return (
|
||||
<div>
|
||||
<Alert color="warning" title={'Select an agent'} />
|
||||
</div>
|
||||
)
|
||||
return <Alert type="warning" message={'Select an agent'} style={{ margin: 10 }} />
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import type { ButtonProps } from '@heroui/react'
|
||||
import { Button, cn } from '@heroui/react'
|
||||
import type { ButtonProps } from 'antd'
|
||||
import { Button } from 'antd'
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props extends ButtonProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
const StyledButton = styled(Button)`
|
||||
height: 36px;
|
||||
width: calc(var(--assistants-width) - 20px);
|
||||
justify-content: flex-start;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
|
||||
const AddButton: FC<Props> = ({ children, className, ...props }) => {
|
||||
&:hover {
|
||||
background-color: var(--color-list-item);
|
||||
}
|
||||
`
|
||||
|
||||
const AddButton: FC<ButtonProps> = ({ ...props }) => {
|
||||
return (
|
||||
<Button
|
||||
<StyledButton
|
||||
{...props}
|
||||
onPress={props.onPress}
|
||||
className={cn(
|
||||
'h-9 w-[calc(var(--assistants-width)-20px)] justify-start rounded-lg bg-transparent px-3 text-[13px] text-[var(--color-text-2)] hover:bg-[var(--color-list-item)]',
|
||||
className
|
||||
)}
|
||||
startContent={<PlusIcon size={16} className="shrink-0" />}>
|
||||
{children}
|
||||
</Button>
|
||||
type="text"
|
||||
onClick={props.onClick}
|
||||
icon={<PlusIcon size={16} style={{ flexShrink: 0 }} />}>
|
||||
{props.children}
|
||||
</StyledButton>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { cn, Tooltip } from '@heroui/react'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
@@ -6,10 +5,12 @@ import AgentSettingsPopup from '@renderer/pages/settings/AgentSettings/AgentSett
|
||||
import { AgentLabel } from '@renderer/pages/settings/AgentSettings/shared'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import type { AgentEntity } from '@renderer/types'
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
|
||||
import { cn } from '@renderer/utils'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Dropdown, Tooltip } from 'antd'
|
||||
import { Bot } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { memo, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
// const logger = loggerService.withContext('AgentItem')
|
||||
@@ -36,45 +37,52 @@ const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) =
|
||||
onPress()
|
||||
}, [clickAssistantToShowTopic, topicPosition, onPress])
|
||||
|
||||
const menuItems: MenuProps['items'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('common.edit'),
|
||||
key: 'edit',
|
||||
icon: <EditIcon size={14} />,
|
||||
onClick: () => AgentSettingsPopup.show({ agentId: agent.id })
|
||||
},
|
||||
{
|
||||
label: t('common.delete'),
|
||||
key: 'delete',
|
||||
icon: <DeleteIcon size={14} className="lucide-custom" />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
window.modal.confirm({
|
||||
title: t('agent.delete.title'),
|
||||
content: t('agent.delete.content'),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => onDelete(agent)
|
||||
})
|
||||
}
|
||||
}
|
||||
],
|
||||
[t, agent, onDelete]
|
||||
)
|
||||
|
||||
return (
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger>
|
||||
<Container onClick={handlePress} isActive={isActive}>
|
||||
<AssistantNameRow className="name" title={agent.name ?? agent.id}>
|
||||
<AgentNameWrapper>
|
||||
<AgentLabel agent={agent} />
|
||||
</AgentNameWrapper>
|
||||
{isActive && (
|
||||
<MenuButton>
|
||||
<SessionCount>{sessions.length}</SessionCount>
|
||||
</MenuButton>
|
||||
)}
|
||||
{!isActive && <BotIcon />}
|
||||
</AssistantNameRow>
|
||||
</Container>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem key="edit" onClick={() => AgentSettingsPopup.show({ agentId: agent.id })}>
|
||||
<EditIcon size={14} />
|
||||
{t('common.edit')}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
onClick={() => {
|
||||
window.modal.confirm({
|
||||
title: t('agent.delete.title'),
|
||||
content: t('agent.delete.content'),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => onDelete(agent)
|
||||
})
|
||||
}}>
|
||||
<DeleteIcon size={14} className="lucide-custom text-danger" />
|
||||
<span className="text-danger">{t('common.delete')}</span>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
<Dropdown
|
||||
menu={{ items: menuItems }}
|
||||
trigger={['contextMenu']}
|
||||
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
|
||||
<Container onClick={handlePress} isActive={isActive}>
|
||||
<AssistantNameRow className="name" title={agent.name ?? agent.id}>
|
||||
<AgentNameWrapper>
|
||||
<AgentLabel agent={agent} />
|
||||
</AgentNameWrapper>
|
||||
{isActive && (
|
||||
<MenuButton>
|
||||
<SessionCount>{sessions.length}</SessionCount>
|
||||
</MenuButton>
|
||||
)}
|
||||
{!isActive && <BotIcon />}
|
||||
</AssistantNameRow>
|
||||
</Container>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -118,7 +126,7 @@ export const MenuButton: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ cla
|
||||
export const BotIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ ...props }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Tooltip content={t('common.agent_one')} delay={500} closeDelay={0}>
|
||||
<Tooltip title={t('common.agent_one')} mouseEnterDelay={0.5}>
|
||||
<MenuButton {...props}>
|
||||
<Bot size={14} className="text-primary" />
|
||||
</MenuButton>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { cn } from '@heroui/react'
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import EmojiIcon from '@renderer/components/EmojiIcon'
|
||||
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
@@ -12,7 +11,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setActiveTopicOrSessionAction } from '@renderer/store/runtime'
|
||||
import type { Assistant, AssistantsSortType } from '@renderer/types'
|
||||
import { getLeadingEmoji, uuid } from '@renderer/utils'
|
||||
import { cn, getLeadingEmoji, uuid } from '@renderer/utils'
|
||||
import { hasTopicPendingRequests } from '@renderer/utils/queue'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Dropdown } from 'antd'
|
||||
|
||||
@@ -7,24 +7,17 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTimer } from '@renderer/hooks/useTimer'
|
||||
import { SessionSettingsPopup } from '@renderer/pages/settings/AgentSettings'
|
||||
import { SessionLabel } from '@renderer/pages/settings/AgentSettings/shared'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { newMessagesActions } from '@renderer/store/newMessage'
|
||||
import type { AgentSessionEntity } from '@renderer/types'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuTrigger
|
||||
} from '@renderer/ui/context-menu'
|
||||
import { loadTopicMessagesThunk, renameAgentSessionIfNeeded } from '@renderer/store/thunk/messageThunk'
|
||||
import type { AgentSessionEntity, Assistant } from '@renderer/types'
|
||||
import { classNames } from '@renderer/utils'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { Tooltip } from 'antd'
|
||||
import { MenuIcon, XIcon } from 'lucide-react'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Dropdown, Tooltip } from 'antd'
|
||||
import { MenuIcon, Sparkles, XIcon } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import React, { memo, startTransition, useEffect, useMemo, useState } from 'react'
|
||||
import React, { memo, startTransition, useDeferredValue, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -45,6 +38,8 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
||||
const activeSessionId = chat.activeSessionIdMap[agentId]
|
||||
const [isConfirmingDeletion, setIsConfirmingDeletion] = useState(false)
|
||||
const { setTimeoutTimer } = useTimer()
|
||||
const [_targetSession, setTargetSession] = useState<AgentSessionEntity>(session)
|
||||
const targetSession = useDeferredValue(_targetSession)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { isEditing, isSaving, editValue, inputRef, startEdit, handleKeyDown, handleValueChange } = useInPlaceEdit({
|
||||
@@ -111,80 +106,98 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
||||
const { topicPosition, setTopicPosition } = useSettings()
|
||||
const singlealone = topicPosition === 'right'
|
||||
|
||||
const menuItems: MenuProps['items'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('common.edit'),
|
||||
key: 'edit',
|
||||
icon: <EditIcon size={14} />,
|
||||
onClick: () => {
|
||||
SessionSettingsPopup.show({
|
||||
agentId,
|
||||
sessionId: session.id
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.auto_rename'),
|
||||
key: 'auto-rename',
|
||||
icon: <Sparkles size={14} />,
|
||||
onClick: () => {
|
||||
const assistant = {} as Assistant
|
||||
const agentSession = { agentId: agentId, sessionId: targetSession.id }
|
||||
dispatch(loadTopicMessagesThunk(sessionTopicId))
|
||||
renameAgentSessionIfNeeded(agentSession, assistant, sessionTopicId, store.getState)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('settings.topic.position.label'),
|
||||
key: 'topic-position',
|
||||
icon: <MenuIcon size={14} />,
|
||||
children: [
|
||||
{
|
||||
label: t('settings.topic.position.left'),
|
||||
key: 'left',
|
||||
onClick: () => setTopicPosition('left')
|
||||
},
|
||||
{
|
||||
label: t('settings.topic.position.right'),
|
||||
key: 'right',
|
||||
onClick: () => setTopicPosition('right')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: t('common.delete'),
|
||||
key: 'delete',
|
||||
icon: <DeleteIcon size={14} className="lucide-custom" />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
onDelete()
|
||||
}
|
||||
}
|
||||
],
|
||||
[agentId, dispatch, onDelete, session.id, sessionTopicId, setTopicPosition, t, targetSession.id]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenuTrigger>
|
||||
<SessionListItem
|
||||
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
|
||||
onClick={isEditing ? undefined : onPress}
|
||||
onDoubleClick={() => startEdit(session.name ?? '')}
|
||||
title={session.name ?? session.id}
|
||||
style={{
|
||||
borderRadius: 'var(--list-item-border-radius)',
|
||||
cursor: isEditing ? 'default' : 'pointer'
|
||||
}}>
|
||||
{isPending && !isActive && <PendingIndicator />}
|
||||
{isFulfilled && !isActive && <FulfilledIndicator />}
|
||||
<SessionNameContainer>
|
||||
{isEditing ? (
|
||||
<SessionEditInput
|
||||
ref={inputRef}
|
||||
value={editValue}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleValueChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
style={{ opacity: isSaving ? 0.5 : 1 }}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<SessionName>
|
||||
<SessionLabel session={session} />
|
||||
</SessionName>
|
||||
<DeleteButton />
|
||||
</>
|
||||
)}
|
||||
</SessionNameContainer>
|
||||
</SessionListItem>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
key="edit"
|
||||
onClick={() => {
|
||||
SessionSettingsPopup.show({
|
||||
agentId,
|
||||
sessionId: session.id
|
||||
})
|
||||
}}>
|
||||
<EditIcon size={14} />
|
||||
{t('common.edit')}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSub>
|
||||
<ContextMenuSubTrigger className="gap-2">
|
||||
<MenuIcon size={14} />
|
||||
{t('settings.topic.position.label')}
|
||||
</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent>
|
||||
<ContextMenuItem key="left" onClick={() => setTopicPosition('left')}>
|
||||
{t('settings.topic.position.left')}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem key="right" onClick={() => setTopicPosition('right')}>
|
||||
{t('settings.topic.position.right')}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSubContent>
|
||||
</ContextMenuSub>
|
||||
<ContextMenuItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
onClick={() => {
|
||||
onDelete()
|
||||
}}>
|
||||
<DeleteIcon size={14} className="lucide-custom text-danger" />
|
||||
<span className="text-danger">{t('common.delete')}</span>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</>
|
||||
<Dropdown
|
||||
menu={{ items: menuItems }}
|
||||
trigger={['contextMenu']}
|
||||
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
|
||||
<SessionListItem
|
||||
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
|
||||
onClick={isEditing ? undefined : onPress}
|
||||
onDoubleClick={() => startEdit(session.name ?? '')}
|
||||
title={session.name ?? session.id}
|
||||
onContextMenu={() => setTargetSession(session)}
|
||||
style={{
|
||||
borderRadius: 'var(--list-item-border-radius)',
|
||||
cursor: isEditing ? 'default' : 'pointer'
|
||||
}}>
|
||||
{isPending && !isActive && <PendingIndicator />}
|
||||
{isFulfilled && !isActive && <FulfilledIndicator />}
|
||||
<SessionNameContainer>
|
||||
{isEditing ? (
|
||||
<SessionEditInput
|
||||
ref={inputRef}
|
||||
value={editValue}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleValueChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||
style={{ opacity: isSaving ? 0.5 : 1 }}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<SessionName>
|
||||
<SessionLabel session={session} />
|
||||
</SessionName>
|
||||
<DeleteButton />
|
||||
</>
|
||||
)}
|
||||
</SessionNameContainer>
|
||||
</SessionListItem>
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Alert, Spinner } from '@heroui/react'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||
import { useCreateDefaultSession } from '@renderer/hooks/agents/useCreateDefaultSession'
|
||||
import { useSessions } from '@renderer/hooks/agents/useSessions'
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
setSessionWaitingAction
|
||||
} from '@renderer/store/runtime'
|
||||
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
|
||||
import { Alert, Spin } from 'antd'
|
||||
import { motion } from 'framer-motion'
|
||||
import { memo, useCallback, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddButton from './AddButton'
|
||||
import SessionItem from './SessionItem'
|
||||
@@ -87,16 +89,18 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="flex h-full items-center justify-center">
|
||||
<Spinner size="lg" />
|
||||
<Spin />
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) return <Alert color="danger" content={t('agent.session.get.error.failed')} />
|
||||
if (error) {
|
||||
return <Alert type="error" message={t('agent.session.get.error.failed')} showIcon style={{ margin: 10 }} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="sessions-tab flex h-full w-full flex-col p-2">
|
||||
<AddButton onPress={createDefaultSession} className="mb-2" isDisabled={creatingSession}>
|
||||
<Container className="sessions-tab">
|
||||
<AddButton onClick={createDefaultSession} disabled={creatingSession} className="-mt-[4px] mb-[6px]">
|
||||
{t('agent.session.add.title')}
|
||||
</AddButton>
|
||||
{/* h-9 */}
|
||||
@@ -118,8 +122,15 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
|
||||
/>
|
||||
)}
|
||||
</DynamicVirtualList>
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 10px;
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
export default memo(Sessions)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user