Compare commits

...

16 Commits

Author SHA1 Message Date
GeorgeDong32
49320936f8 fix(token): include assistant reasoning token estimates and params 2025-11-07 23:28:15 +08:00
Phantom
e268e69597 refactor(config): centralize home directory constant to shared config (#11158)
Replace hardcoded '.cherrystudio' directory references with HOME_CHERRY_DIR constant
2025-11-07 22:24:05 +08:00
kangfenmao
10e78ac60e refactor(MCPSettings): update styled components and enhance server synchronization
- Changed RightContainer from Scrollbar to a standard div for layout adjustments.
- Updated DetailContainer to use Scrollbar for improved scrolling behavior.
- Modified server synchronization logic across multiple providers to include allServers in the results, enhancing server management capabilities.
- Refactored provider configurations to ensure consistency and support for new server data structure.
2025-11-07 19:22:58 +08:00
kangfenmao
44b2b859da feat(MCPRouter): add MCPRouter provider support and integration
- Introduced MCPRouter provider with token management and server synchronization functionalities.
- Added MCPRouter logo to settings page for visual representation.
- Updated provider configuration to include MCPRouter details and API interactions.
- Implemented functions for saving, retrieving, and clearing MCPRouter tokens, along with server synchronization logic.
2025-11-07 18:41:15 +08:00
kangfenmao
bfef0c5580 feat(MCPSettings): enhance MCP server management and localization support
- Updated auto-translation script to allow configurable max concurrent translations and delay via environment variables.
- Added new translations for "discover", "fetch", "marketplaces", "providers", and "servers" across multiple locales (en-us, zh-cn, zh-tw, de-de, el-gr, es-es, fr-fr, ja-jp, pt-pt, ru-ru).
- Improved MCPSettings UI by adjusting layout and adding a new provider settings component for better server management.
- Refactored MCP server list and market list components for improved usability and styling consistency.
2025-11-07 18:01:55 +08:00
fullex
1e8055031a Merge branch 'main' of github.com:CherryHQ/cherry-studio 2025-11-07 12:02:35 +08:00
fullex
8e33ff8d90 docs: update test plan documentation to clarify upgrade behavior for RC and Beta channels 2025-11-07 12:02:28 +08:00
kangfenmao
a619000340 chore: update v1.7.0-beta.4 release notes
Update electron-builder.yml with release notes covering:
- UI framework upgrade with improved performance and UX
- New features: AWS Bedrock API key support, SophNet provider, auto session rename, TopP parameter, and reasoning effort control
- Improvements to UI components, quick panel, painting models, system shutdown handling, and package size optimization
- Bug fixes for provider support, i18n translations, and API issues
2025-11-06 20:51:03 +08:00
kangfenmao
78278ce96d refactor: remove heroui
commit 7c8bf8b591
Author: defi-failure <159208748+defi-failure@users.noreply.github.com>
Date:   Thu Nov 6 17:59:38 2025 +0800

    fix: add token usage to agent session message

commit ff8e5ddd27
Author: defi-failure <159208748+defi-failure@users.noreply.github.com>
Date:   Thu Nov 6 17:25:54 2025 +0800

    fix: close prompt stream when finish or error chunk received

commit 530e6516fd
Author: defi-failure <159208748+defi-failure@users.noreply.github.com>
Date:   Thu Nov 6 17:19:53 2025 +0800

    chore: code cleanup

commit ab21c0d56c
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 16:13:36 2025 +0800

    feat(SessionItem): implement auto-rename feature for sessions and improve context menu handling

    - Added a new context menu option to automatically rename sessions based on topics.
    - Introduced useDeferredValue for managing target session state.
    - Updated imports to include necessary thunk actions and components.
    - Enhanced API service to handle optional assistant model in message summary fetching.
    - Exported renameAgentSessionIfNeeded function for better accessibility in the store.

commit 21ea8ccf37
Merge: ab7b207d2 816a92c60
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 15:29:09 2025 +0800

    Merge branch 'main' of github.com:CherryHQ/cherry-studio into refactor/heroui-antd

    # Conflicts:
    #	src/renderer/src/pages/home/Tabs/components/AddButton.tsx
    #	src/renderer/src/pages/home/Tabs/components/SessionItem.tsx
    #	src/renderer/src/pages/home/Tabs/components/Sessions.tsx
    #	src/renderer/src/pages/home/Tabs/components/Topics.tsx
    #	src/renderer/src/pages/paintings/NewApiPage.tsx

commit ab7b207d29
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 14:50:05 2025 +0800

    refactor: streamline event listener management in useAppInit and update ToolPermissionRequestCard styling

commit 3834c5d402
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 14:21:25 2025 +0800

    refactor: enhance API server state management and remove unused initialization in useAppInit

commit a64b94a41f
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 13:21:58 2025 +0800

    refactor: update OpenAPI documentation paths to include subdirectories for better route coverage

commit 2e0ff28505
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 12:26:09 2025 +0800

    refactor: center align columns in InstalledPluginsList and set AntTable size to small

commit 84bf94e2ff
Author: defi-failure <159208748+defi-failure@users.noreply.github.com>
Date:   Thu Nov 6 12:06:09 2025 +0800

    refactor: align create agent model selection with edit agent

commit 84f2281506
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 11:29:32 2025 +0800

    refactor: integrate API server functionality into various components and enhance user notifications

commit 4e01210df4
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 10:56:38 2025 +0800

    refactor: replace ContextMenu with Dropdown in AgentItem and SessionItem components for improved context menu handling

commit 9df38c7e83
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 10:27:30 2025 +0800

    refactor: update AddButton styling to use CSS variable for border radius and remove unused settings hook

commit 251c269ab3
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 10:11:21 2025 +0800

    refactor: remove unused error handling alerts from AssistantsTab component

commit 9b9640d8d1
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 10:07:26 2025 +0800

    refactor: adjust margin styling for UnifiedAddButton component

commit edd6b11aa7
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 10:04:01 2025 +0800

    refactor: update AddButton styling based on topic position and clean up CSS for root element

commit 1c0de625d8
Author: kangfenmao <kangfenmao@qq.com>
Date:   Thu Nov 6 09:56:42 2025 +0800

    fix: update assistant addition messages for multiple languages

commit 0ea4dd4e3a
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 21:01:24 2025 +0800

    fix: init message api err

commit f3bbd4ed44
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 20:42:49 2025 +0800

    refactor: remove heroui

commit d01609fc36
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 19:08:41 2025 +0800

    refactor: migrate heroui/toast to antd message

commit f4b14dfc10
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 18:51:29 2025 +0800

    refactor: enhance Sessions component layout with styled Scrollbar and adjust UnifiedAddButton margins

commit 6ae5f69163
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 18:44:13 2025 +0800

    refactor: update PluginSettings and ToolingSettings for improved layout and functionality

commit fcb0020787
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 18:29:52 2025 +0800

    wip

commit 02265f369e
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 17:26:39 2025 +0800

    fix: error block related

commit 5e22d9d36f
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 17:14:25 2025 +0800

    fix: note head nav related

commit 3f52b7766a
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 16:45:49 2025 +0800

    chore: remove dead code

commit 484622f12b
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 16:43:12 2025 +0800

    chore: remove dead code

commit 2bceb302e0
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 15:33:25 2025 +0800

    fix: tool setting related

commit 5c455f25eb
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 13:59:33 2025 +0800

    chore: remove dead code

commit d1d1dbc046
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 13:51:41 2025 +0800

    fix: tool permission card related

commit bf4ec23ef7
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 12:22:53 2025 +0800

    fix: remove button and modal renaming

commit 47db5baeb1
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 12:20:36 2025 +0800

    fix: plugin setting related

commit 81fecce552
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 12:16:42 2025 +0800

    refactor: enhance ChatNavbarContent structure by replacing Breadcrumbs with custom layout and adding separators

commit fc64b6c611
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 12:10:48 2025 +0800

    refactor: simplify MessageAgentTools component structure by removing unnecessary wrapper div

commit e0f383a050
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 12:08:32 2025 +0800

    fix: update button classes in AddAssistantOrAgentPopup for improved cursor behavior

commit 720284262f
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 12:06:58 2025 +0800

    refactor: update AgentModal to use TopView for improved modal management and enhance form structure

commit b334a2c5be
Author: kangfenmao <kangfenmao@qq.com>
Date:   Wed Nov 5 11:40:47 2025 +0800

    refactor: replace UpdateDialog with UpdateDialogPopup for better modal handling

commit 468aebd632
Author: dev <verc20.dev@proton.me>
Date:   Wed Nov 5 10:56:40 2025 +0800

    fix: plugins related wip

commit bd4a979f62
Author: dev <verc20.dev@proton.me>
Date:   Tue Nov 4 17:46:14 2025 +0800

    fix: add button related

commit b3316a4dc8
Author: dev <verc20.dev@proton.me>
Date:   Tue Nov 4 17:18:31 2025 +0800

    fix: agent tool result related components

commit 6ca7597a98
Author: dev <verc20.dev@proton.me>
Date:   Tue Nov 4 11:12:01 2025 +0800

    fix: lint

commit 7d0f0b38a6
Author: kangfenmao <kangfenmao@qq.com>
Date:   Tue Nov 4 09:56:32 2025 +0800

    wip

commit 96a607a410
Author: kangfenmao <kangfenmao@qq.com>
Date:   Mon Nov 3 20:23:25 2025 +0800

    wip

commit 235ad16252
Author: kangfenmao <kangfenmao@qq.com>
Date:   Mon Nov 3 20:08:45 2025 +0800

    wip

commit f23fe1b9e9
Author: kangfenmao <kangfenmao@qq.com>
Date:   Mon Nov 3 19:15:01 2025 +0800

    wip

commit 28fac543fc
Author: kangfenmao <kangfenmao@qq.com>
Date:   Mon Nov 3 18:39:39 2025 +0800

    wip

commit 3cc7ee01e2
Author: kangfenmao <kangfenmao@qq.com>
Date:   Mon Nov 3 17:33:13 2025 +0800

    wip

commit 37bdf9e508
Author: kangfenmao <kangfenmao@qq.com>
Date:   Sat Nov 1 19:16:58 2025 +0800

    wip

commit 1bf5104f97
Author: kangfenmao <kangfenmao@qq.com>
Date:   Sat Nov 1 12:12:01 2025 +0800

    wip
2025-11-06 18:27:43 +08:00
Phantom
76483d828e ci(i18n): change auto i18n workflow to run weekly (#11152)
* ci(i18n): change auto i18n workflow to run weekly

Update the workflow to run on a weekly schedule instead of on pull request events.
Also simplify the workflow by using yarn for dependency management and create a PR
for changes instead of committing directly to the branch.

* ci(github-actions): improve workflow step names with emojis

Use emojis in step names to enhance readability and visual scanning of workflow logs

* ci(workflow): prevent committing package.json and yarn.lock changes in i18n workflow
2025-11-06 18:25:04 +08:00
Jake Jia
816a92c609 feat(app-menu): add full i18n support and sync lanuage with app language settings (#11131)
Previously, the macOS menu bar was always displayed in English regardless of
system language or in-app language settings. This change enables the menu bar
to dynamically follow the application's language preference.

Key changes:
- Add language change listener to automatically update menu when user switches language
- Refactor AppMenuService with proper subscription management and cleanup
- Add appMenu translations for en-us, zh-cn, and zh-tw locales
- Implement destroy method to prevent memory leaks from config subscriptions
- Convert all menu items (File, Edit, View, Window, Help) to use localized labels

The menu bar now respects the in-app language setting and updates in real-time
when users change their preferences, providing a consistent multilingual experience.
2025-11-06 14:46:42 +08:00
beyondkmp
83e4d4363f fix: add Perplexity provider support and update API host formatting (#11162)
* feat: add Perplexity provider support and update API host formatting

- Introduced `isPerplexityProvider` function to identify Perplexity providers.
- Updated `formatProviderApiHost` to handle Perplexity provider API host formatting.
- Added unit tests for Perplexity provider configuration to ensure correct API host formatting behavior.

* fix: add 'perplexity' to unsupported API version providers list
2025-11-06 10:43:33 +08:00
Phantom
1103449a4f fix: wrong migration in #10727 (#11151) 2025-11-05 14:33:07 +08:00
beyondkmp
56c7a7f066 🐛 fix: resolve TypeScript type conflicts and React hooks warnings (#11148)
* 🐛 fix: resolve TypeScript type conflicts and React hooks warnings

- Add @smithy/types@4.7.1 to resolutions to unify AWS SDK dependencies
- Wrap updatePaintingState in useCallback to fix exhaustive-deps warning
- Fix AWS Bedrock client type incompatibility issues

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(i18n): Auto update translations for PR #11148

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2025-11-05 14:19:14 +08:00
Phantom
caa59c4c50 refactor(Topics & Sessions): Style and code structure adjustments (#10868)
* refactor(Tabs): extract shared styled components into separate file

Move common styled components (ListItem, ListItemNameContainer, ListItemName, ListItemEditInput) from SessionItem.tsx and Topics.tsx into shared.tsx to improve code reuse and maintainability

* refactor(components): extract ListContainer component for shared tab layouts

Create reusable ListContainer component to standardize layout styling across tabs
Replace manual div containers in Sessions and Topics components with new ListContainer

* refactor(ListItem): convert styled component to Tailwind CSS function component

- Convert ListItem from styled-components to Tailwind CSS function component
- Maintain all original styling and hover/active states
- Use HTMLDivElement props interface for proper TypeScript typing
- Preserve CSS custom properties for theme variables

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(ListItemNameContainer): convert styled component to Tailwind CSS function component

- Convert ListItemNameContainer from styled-components to Tailwind CSS function component
- Simplify layout styles using Tailwind's utility classes
- Use HTMLDivElement props interface for proper TypeScript typing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(ListItemName): convert styled component to Tailwind CSS function component

- Convert ListItemName from styled-components to Tailwind CSS function component
- Use inline styles for webkit-specific line clamping properties
- Remove complex animations from component definition (can be added via CSS classes)
- Use HTMLDivElement props interface for proper TypeScript typing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(ListItemEditInput): convert styled component to Tailwind CSS function component

- Convert ListItemEditInput from styled-components to Tailwind CSS function component
- Use proper InputHTMLAttributes type for input elements
- Remove styled-components import as no longer needed
- Maintain all original styling using Tailwind utility classes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(components): improve type safety and class ordering in shared components

- Replace HTMLAttributes with more specific ComponentProps types
- Reorder class names for better readability and consistency

* refactor(components): update styling and class handling in list items

- Replace deprecated classNames utility with cn from @heroui/react
- Consolidate style properties into className using cn
- Improve CSS selector syntax for better specificity
- Standardize padding and border radius values

* Revert "refactor(ListItemName): convert styled component to Tailwind CSS function component"

This reverts commit 196136068d.

* style(shared): increase font size and remove redundant padding

The font size was increased from 13px to 14px for better readability. Redundant padding in ListItemEditInput was removed to maintain consistent styling.

* refactor(AddButton): simplify component by removing FC type and inline props

Remove unnecessary FC type declaration and inline the Props interface with ButtonProps. Also clean up prop spreading by moving it to the end of the component.

* style(Topics): remove redundant className and add overflow styles

* refactor(components): extract MenuButton to shared components

Move MenuButton implementation from individual components to shared module to reduce code duplication and improve maintainability

* refactor(PendingIndicator): convert styled component to Tailwind CSS function component

- Convert PendingIndicator from styled-components to Tailwind CSS function component
- Use ComponentPropsWithoutRef<'div'> for consistent TypeScript typing
- Replace styled-components attrs with Tailwind animate-pulse class
- Use CSS custom properties for pulse-size variable
- Remove styled-components import as no longer needed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(components): replace styled indicators with shared StatusIndicator

Consolidate PendingIndicator and FulfilledIndicator into a single StatusIndicator component with variant support

* style(shared.tsx): adjust border styles for singlealone active state

* refactor: use type-only imports for react props

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-05 14:14:40 +08:00
fullex
2546dfbe5d chore: update Node.js version to 22 and Yarn version to 4.9.1 across workflows and documentation 2025-11-05 12:54:30 +08:00
154 changed files with 4272 additions and 5883 deletions

View File

@@ -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."

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -11,6 +11,8 @@
用户可以在软件的`设置`-`关于`中,开启“测试计划”并选择版本通道。请注意“测试计划”的版本无法保证数据的一致性,请使用前一定要备份数据。
用户选择RC版通道或Beta版通道后若发布了正式版仍旧会升级到正式版。
用户在测试过程中发现的BUG欢迎提交issue或通过其他渠道反馈。用户的反馈对我们非常重要。
## 开发者指南

View File

@@ -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-->

View File

@@ -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",

View File

@@ -470,3 +470,6 @@ export const MACOS_TERMINALS_WITH_COMMANDS: TerminalConfigWithCommand[] = [
})
}
]
// resources/scripts should be maintained manually
export const HOME_CHERRY_DIR = '.cherrystudio'

View File

@@ -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

View File

@@ -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) {

View File

@@ -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')
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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) => {

View File

@@ -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')
}
}
}

View File

@@ -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 {

View File

@@ -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')
}
/**

View File

@@ -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() {

View File

@@ -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
}

View File

@@ -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>
)

View File

@@ -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('')
})
})

View File

@@ -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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -41,11 +41,11 @@ body,
margin: 0;
}
/* #root {
#root {
display: flex;
flex-direction: row;
flex: 1;
} */
}
body {
display: flex;

View File

@@ -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 {

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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'

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'

View File

@@ -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">

View File

@@ -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>
)
}

View 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;
}
}
`

View File

@@ -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;
}
}
`

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -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
)
}

View File

@@ -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 }) => (

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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>
)
}

View File

@@ -1 +0,0 @@
export { AllowedToolsSelect } from './AllowedToolsSelect'

View File

@@ -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)) {

View File

@@ -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 }

View File

@@ -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

View File

@@ -1,2 +0,0 @@
import { heroui } from '@heroui/react'
export default heroui()

View File

@@ -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

View File

@@ -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()

View File

@@ -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(() => {

View File

@@ -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())

View File

@@ -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)",

View File

@@ -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)",

View File

@@ -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)",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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>
)
}, [])

View File

@@ -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>

View File

@@ -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>
]}

View File

@@ -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>
)
}

View File

@@ -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'

View File

@@ -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
}
}

View File

@@ -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>
}
}

View File

@@ -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}
</>
)
}
}

View File

@@ -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>
}
}

View File

@@ -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>
}
}

View File

@@ -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>
}
}

View File

@@ -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>
)
}
}

View File

@@ -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>
}
}

View File

@@ -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
}
}

View File

@@ -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>
)
)
}
}

View File

@@ -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>
}
}

View File

@@ -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>
)
}
}

View File

@@ -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>
)
)
}
}

View File

@@ -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>
)
)
}
}

View File

@@ -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>
}
}

View File

@@ -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>
}
}

View File

@@ -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>
}
}

View File

@@ -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]}
/>
)
}

View File

@@ -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 && (

View File

@@ -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}

View File

@@ -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>

View File

@@ -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 (

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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'

View File

@@ -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>
)
}

View File

@@ -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