Compare commits

...

74 Commits

Author SHA1 Message Date
defi-failure
7c8bf8b591 fix: add token usage to agent session message 2025-11-06 17:59:38 +08:00
defi-failure
ff8e5ddd27 fix: close prompt stream when finish or error chunk received 2025-11-06 17:27:03 +08:00
defi-failure
530e6516fd chore: code cleanup 2025-11-06 17:19:53 +08:00
kangfenmao
ab21c0d56c 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.
2025-11-06 16:15:06 +08:00
kangfenmao
21ea8ccf37 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
2025-11-06 15:29:09 +08:00
kangfenmao
ab7b207d29 refactor: streamline event listener management in useAppInit and update ToolPermissionRequestCard styling 2025-11-06 14:50:05 +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
kangfenmao
3834c5d402 refactor: enhance API server state management and remove unused initialization in useAppInit 2025-11-06 14:21:25 +08:00
kangfenmao
a64b94a41f refactor: update OpenAPI documentation paths to include subdirectories for better route coverage 2025-11-06 13:21:58 +08:00
kangfenmao
2e0ff28505 refactor: center align columns in InstalledPluginsList and set AntTable size to small 2025-11-06 12:27:32 +08:00
defi-failure
84bf94e2ff refactor: align create agent model selection with edit agent 2025-11-06 12:06:09 +08:00
kangfenmao
84f2281506 refactor: integrate API server functionality into various components and enhance user notifications 2025-11-06 11:29:32 +08:00
kangfenmao
4e01210df4 refactor: replace ContextMenu with Dropdown in AgentItem and SessionItem components for improved context menu handling 2025-11-06 10:56:38 +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
kangfenmao
9df38c7e83 refactor: update AddButton styling to use CSS variable for border radius and remove unused settings hook 2025-11-06 10:27:30 +08:00
kangfenmao
251c269ab3 refactor: remove unused error handling alerts from AssistantsTab component 2025-11-06 10:14:35 +08:00
kangfenmao
9b9640d8d1 refactor: adjust margin styling for UnifiedAddButton component 2025-11-06 10:07:26 +08:00
kangfenmao
edd6b11aa7 refactor: update AddButton styling based on topic position and clean up CSS for root element 2025-11-06 10:04:01 +08:00
kangfenmao
1c0de625d8 fix: update assistant addition messages for multiple languages 2025-11-06 09:56:42 +08:00
dev
0ea4dd4e3a fix: init message api err 2025-11-05 21:01:24 +08:00
dev
f3bbd4ed44 refactor: remove heroui 2025-11-05 20:42:49 +08:00
dev
d01609fc36 refactor: migrate heroui/toast to antd message 2025-11-05 20:12:07 +08:00
kangfenmao
f4b14dfc10 refactor: enhance Sessions component layout with styled Scrollbar and adjust UnifiedAddButton margins 2025-11-05 18:51:29 +08:00
kangfenmao
6ae5f69163 refactor: update PluginSettings and ToolingSettings for improved layout and functionality 2025-11-05 18:44:13 +08:00
kangfenmao
fcb0020787 wip 2025-11-05 18:29:52 +08:00
dev
02265f369e fix: error block related 2025-11-05 17:26:39 +08:00
dev
5e22d9d36f fix: note head nav related 2025-11-05 17:14:25 +08:00
dev
3f52b7766a chore: remove dead code 2025-11-05 16:45:49 +08:00
dev
484622f12b chore: remove dead code 2025-11-05 16:43:12 +08:00
dev
2bceb302e0 fix: tool setting related 2025-11-05 15:33:25 +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
dev
5c455f25eb chore: remove dead code 2025-11-05 13:59:33 +08:00
dev
d1d1dbc046 fix: tool permission card related 2025-11-05 13:51:41 +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
dev
bf4ec23ef7 fix: remove button and modal renaming 2025-11-05 12:22:53 +08:00
dev
47db5baeb1 fix: plugin setting related 2025-11-05 12:20:36 +08:00
kangfenmao
81fecce552 refactor: enhance ChatNavbarContent structure by replacing Breadcrumbs with custom layout and adding separators 2025-11-05 12:16:42 +08:00
kangfenmao
fc64b6c611 refactor: simplify MessageAgentTools component structure by removing unnecessary wrapper div 2025-11-05 12:10:48 +08:00
kangfenmao
e0f383a050 fix: update button classes in AddAssistantOrAgentPopup for improved cursor behavior 2025-11-05 12:08:32 +08:00
kangfenmao
720284262f refactor: update AgentModal to use TopView for improved modal management and enhance form structure 2025-11-05 12:06:58 +08:00
kangfenmao
b334a2c5be refactor: replace UpdateDialog with UpdateDialogPopup for better modal handling 2025-11-05 11:40:47 +08:00
dev
468aebd632 fix: plugins related wip 2025-11-05 10:56:40 +08:00
beyondkmp
5fea202a7d fix: add PowerMonitorService for system shutdown handling (#11115)
* feat: add PowerMonitorService for system shutdown handling

- Add PowerMonitorService to monitor system shutdown events
- Use @paymoapp/electron-shutdown-handler for Windows platform
- Use Electron's powerMonitor for macOS and Linux platforms
- Support registering multiple shutdown handlers via dependency injection
- Register shutdown handlers in ipc.ts to disable auto-update and save data

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

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

* format code

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-04 18:56:09 +08:00
dev
bd4a979f62 fix: add button related 2025-11-04 17:46:14 +08:00
dev
b3316a4dc8 fix: agent tool result related components 2025-11-04 17:18:31 +08:00
fullex
7dce1d776b feat: app's version history log (#11097)
* feat: integrate version tracking in app initialization

- Added versionService to record the current version during app startup.
- This change prepares for upcoming data refactoring in version 2.

* fix: lint from other PRs & format

* feat: enhance version tracking with meaningful change detection

- Updated VersionService to check for changes in version, OS, environment, packaged status, and install mode before recording a new entry.
- Improved logging to reflect whether version information has changed or remained the same.
2025-11-04 14:13:07 +08:00
beyondkmp
346af4d338 fix: add CherryAI provider support and update API host formatting (#11135)
* fix: add CherryAI provider support and update API host formatting

* format code

* add ut

* format code
2025-11-04 12:59:14 +08:00
dev
6ca7597a98 fix: lint 2025-11-04 11:12:01 +08:00
kangfenmao
7d0f0b38a6 wip 2025-11-04 09:56:32 +08:00
Zephyr
abd5d3b96f feat: amazon bedrock request use bedrock api key (#10727)
* feat: amazon bedrock request use bedrock api key

* feat: ai-core/provider support bedrock api key

* refactor: extract AWS Bedrock auth type and remove redundant state

* feat: add bedrock reasoning support

Add AWS Bedrock-specific reasoning parameter handling to support Extended Thinking feature for Claude models via Bedrock API.

Changes:
- Add `buildBedrockProviderOptions` function in options.ts to handle Bedrock-specific provider options
- Add `getBedrockReasoningParams` function in reasoning.ts to generate reasoning config with budget tokens
- Register 'bedrock' case in provider options switch to route to Bedrock-specific builder
- Reuse `getAnthropicThinkingBudget` helper for consistent token budget calculation

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

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

* feat: add migration for Bedrock auth type and API key fields

* refactor: replace any type with BedrockRuntimeClientConfig in AWS Bedrock client

* fix: bug fix

* fix: lint error

* fix: bedrock reasoning

* chore: bump persisted reducer version to 171

* Update src/renderer/src/store/migrate.ts

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: icarus <eurfelux@gmail.com>
2025-11-03 21:05:10 +08:00
Phantom
49bd298d37 feat(InputbarTools): add reasoning effort button to quick panel (#10959)
Add new menu item with lightbulb icon that opens the reasoning effort quick panel when clicked
2025-11-03 20:36:52 +08:00
Phantom
714a28ac29 fix(QuickPanel): Hide the options that should be hidden in the quick panel. (#10931)
* feat(QuickPanel): add hidden property to list items

Add support for hiding QuickPanel items by introducing a hidden property. This allows conditional visibility of items like the knowledge base button based on application state.

* docs(types): clarify settings field comment in Assistant type
2025-11-03 20:34:24 +08:00
kangfenmao
96a607a410 wip 2025-11-03 20:23:25 +08:00
kangfenmao
235ad16252 wip 2025-11-03 20:08:45 +08:00
kangfenmao
f23fe1b9e9 wip 2025-11-03 19:15:01 +08:00
kangfenmao
28fac543fc wip 2025-11-03 18:39:39 +08:00
beyondkmp
0cf81c04c8 chore: update electron-builder.yml to exclude additional configuration files from build (#11129)
* chore: update electron-builder.yml to exclude additional configuration files from build

* delete all hide files
2025-11-03 17:54:29 +08:00
kangfenmao
3cc7ee01e2 wip 2025-11-03 17:33:13 +08:00
kangfenmao
37bdf9e508 wip 2025-11-03 17:04:33 +08:00
kangfenmao
1bf5104f97 wip 2025-11-03 17:03:25 +08:00
kangfenmao
4186e9c990 feat: add support for TopP in model capabilities and update parameter builder to utilize it 2025-11-03 16:37:12 +08:00
kangfenmao
d8f68a6056 feat: initialize painting model with first available option and update default provider to 'cherryin' 2025-11-03 15:12:58 +08:00
kangfenmao
11bf50e722 fix(i18n): improve label retrieval for paintings image size options 2025-11-03 14:45:21 +08:00
kangfenmao
32a84311aa feat: add SophNet LLM provider 2025-11-03 13:28:40 +08:00
beyondkmp
6eaa2b2461 refactor: remove main window dependency from PythonService and utilize WindowService for window management (#11116)
* refactor: remove main window dependency from PythonService and utilize WindowService for window management

* format code
2025-11-03 13:09:40 +08:00
defi-failure
9f00f00546 chore: update v1.7.0-beta.3 release notes (#11105)
* chore: update v1.7.0-beta.3 release notes

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

* fix: code lint error

---------

Co-authored-by: GitHub Action <action@github.com>
2025-11-02 22:28:36 +08:00
SuYao
bd94d23343 refactor:Unify the naming of configuration fields in thinking, change to using underscore style. (#11106)
* refactor:Unify the naming of configuration fields in thinking, change to using underscore style.

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

* chore: lint

* fix: typecheck

---------

Co-authored-by: GitHub Action <action@github.com>
2025-11-02 19:24:23 +08:00
chenxue
5f1c14e2c0 fix(aihubmix): fix default rules missing app code (#11100)
* add imagen

* Update aihubmix.ts

* fix type

---------

Co-authored-by: zhaochenxue <zhaochenxue@bixin.cn>
2025-11-02 17:03:05 +08:00
dependabot[bot]
cdc12d5092 ci(deps): bump actions/stale from 9 to 10 (#11088)
Bumps [actions/stale](https://github.com/actions/stale) from 9 to 10.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v9...v10)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '10'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-02 08:44:53 +08:00
dependabot[bot]
e5967fd874 ci(deps): bump actions/upload-artifact from 4 to 5 (#11089)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-02 08:44:41 +08:00
dependabot[bot]
e2f1d80697 ci(deps): bump actions/setup-node from 4 to 6 (#11090)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-02 08:44:28 +08:00
SuYao
28bc89ac7c perf: optimize QR code generation and connection info for phone LAN export (#11086)
* Increase QR code margin for better scanning reliability

- Change QRCodeSVG marginSize from 2 to 4 pixels
- Maintains same QR code size (160px) and error correction level (Q)
- Improves readability and scanning success rate on mobile devices

* Optimize QR code generation and connection info for phone LAN export

- Increase QR code size to 180px and reduce error correction to 'L' for better mobile scanning
- Replace hardcoded logo path with AppLogo config and increase logo size to 60px
- Simplify connection info by removing candidates array and using only essential IP/port data

* Optimize QR code data structure for LAN connection

- Compress IP addresses to numeric format to reduce QR code complexity
- Use compact array format instead of verbose JSON object structure
- Remove debug logging to streamline connection flow

* feat: 更新 WebSocket 状态和候选者响应类型,优化连接信息处理

* Increase QR code size and error correction for better scanning

- Increase QR code size from 180px to 300px for improved readability
- Change error correction level from L (low) to H (high) for better reliability
- Reduce logo size from 60px to 40px to accommodate larger QR data
- Increase margin size from 1 to 2 for better border clearance

* 调整二维码大小和图标尺寸以优化扫描体验

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

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

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

---------

Co-authored-by: GitHub Action <action@github.com>
2025-11-01 12:13:11 +08:00
159 changed files with 5080 additions and 7828 deletions

View File

@@ -27,9 +27,9 @@ jobs:
ref: ${{ github.event.pull_request.head.ref }}
- name: 📦 Setting Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 22
package-manager-cache: false
- name: 📦 Install dependencies in isolated directory

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:
@@ -54,9 +54,9 @@ jobs:
- name: Setup Node.js
if: steps.check_time.outputs.should_delay == 'false'
uses: actions/setup-node@v4
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'
@@ -121,9 +121,9 @@ jobs:
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
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

@@ -21,7 +21,7 @@ jobs:
contents: none
steps:
- name: Close needs-more-info issues
uses: actions/stale@v9
uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: 'needs-more-info'
@@ -42,7 +42,7 @@ jobs:
days-before-pr-close: -1
- name: Close inactive issues
uses: actions/stale@v9
uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: ${{ env.daysBeforeStale }}

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
@@ -56,9 +56,9 @@ jobs:
ref: main
- name: Install Node.js
uses: actions/setup-node@v5
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
@@ -208,7 +208,7 @@ jobs:
echo "总计: $(find renamed-artifacts -type f | wc -l) 个文件"
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: cherry-studio-nightly-${{ steps.date.outputs.date }}-${{ matrix.os }}
path: renamed-artifacts/*

View File

@@ -24,12 +24,12 @@ jobs:
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v5
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*.*.*
@@ -47,9 +47,9 @@ jobs:
npm version "$VERSION" --no-git-tag-version --allow-same-version
- name: Install Node.js
uses: actions/setup-node@v5
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

@@ -21,6 +21,8 @@ files:
- "**/*"
- "!**/{.vscode,.yarn,.yarn-lock,.github,.cursorrules,.prettierrc}"
- "!electron.vite.config.{js,ts,mjs,cjs}}"
- "!.*"
- "!components.json"
- "!**/{.eslintignore,.eslintrc.js,.eslintrc.json,.eslintcache,root.eslint.config.js,eslint.config.js,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,eslint.config.mjs,dev-app-update.yml,CHANGELOG.md,README.md,biome.jsonc}"
- "!**/{.env,.env.*,.npmrc,pnpm-lock.yaml}"
- "!**/{tsconfig.json,tsconfig.tsbuildinfo,tsconfig.node.json,tsconfig.web.json}"
@@ -161,6 +163,9 @@ releaseInfo:
- 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
@@ -188,6 +193,9 @@ releaseInfo:
- 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
<!--LANG:zh-CN-->
v1.7.0-beta.3 新特性
@@ -218,6 +226,9 @@ releaseInfo:
- MCP 确认:添加激活协议安装的 MCP 服务器时的确认模态框
- 翻译:增强翻译脚本的并发和验证功能
- Electron & Vite更新至 Electron 38 和 Vite 4.0.1
- 二维码生成:优化手机局域网导出性能
- 企业设置:在关于设置中添加企业部分
- 助手/Agent 弹窗:增强添加助手和 Agent 的界面
Claude Code 工具改进:
- GlobTool现在计算行数而不是文件数提供更清晰的输出
@@ -245,4 +256,7 @@ releaseInfo:
- 修复 reranker API 错误响应捕获
- 修复右键粘贴文件内容到输入栏
- 修复 aiCore 中的 minimax-m2 支持
- 修复 Azure embedding 问题
- 修复 Agent 编辑模态框加载竞态条件
- 修复文件路径更新时防抖保存取消问题
<!--LANG:END-->

View File

@@ -82,6 +82,7 @@
"@libsql/client": "0.14.0",
"@libsql/win32-x64-msvc": "^0.4.7",
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
"@paymoapp/electron-shutdown-handler": "^1.1.2",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"express": "^5.1.0",
"font-list": "^2.0.0",
@@ -113,9 +114,9 @@
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@anthropic-ai/sdk": "^0.41.0",
"@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch",
"@aws-sdk/client-bedrock": "^3.840.0",
"@aws-sdk/client-bedrock-runtime": "^3.840.0",
"@aws-sdk/client-s3": "^3.840.0",
"@aws-sdk/client-bedrock": "^3.910.0",
"@aws-sdk/client-bedrock-runtime": "^3.910.0",
"@aws-sdk/client-s3": "^3.910.0",
"@biomejs/biome": "2.2.4",
"@cherrystudio/ai-core": "workspace:^1.0.0-alpha.18",
"@cherrystudio/embedjs": "^0.1.31",
@@ -146,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",
@@ -348,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",
@@ -373,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

@@ -31,3 +31,16 @@ export type WebviewKeyEvent = {
shift: boolean
alt: boolean
}
export interface WebSocketStatusResponse {
isRunning: boolean
port?: number
ip?: string
clientConnected: boolean
}
export interface WebSocketCandidatesResponse {
host: string
interface: string
priority: number
}

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

@@ -21,6 +21,7 @@ import { appMenuService } from './services/AppMenuService'
import { configManager } from './services/ConfigManager'
import mcpService from './services/MCPService'
import { nodeTraceService } from './services/NodeTraceService'
import powerMonitorService from './services/PowerMonitorService'
import {
CHERRY_STUDIO_PROTOCOL,
handleProtocolUrl,
@@ -30,6 +31,7 @@ import {
import selectionService, { initSelectionService } from './services/SelectionService'
import { registerShortcuts } from './services/ShortcutService'
import { TrayService } from './services/TrayService'
import { versionService } from './services/VersionService'
import { windowService } from './services/WindowService'
import { initWebviewHotkeys } from './services/WebviewService'
@@ -110,6 +112,10 @@ if (!app.requestSingleInstanceLock()) {
// Some APIs can only be used after this event occurs.
app.whenReady().then(async () => {
// Record current version for tracking
// A preparation for v2 data refactoring
versionService.recordCurrentVersion()
initWebviewHotkeys()
// Set app user model id for windows
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
@@ -127,6 +133,7 @@ if (!app.requestSingleInstanceLock()) {
appMenuService?.setupApplicationMenu()
nodeTraceService.init()
powerMonitorService.init()
app.on('activate', function () {
const mainWindow = windowService.getMainWindow()

View File

@@ -50,6 +50,7 @@ import * as NutstoreService from './services/NutstoreService'
import ObsidianVaultService from './services/ObsidianVaultService'
import { ocrService } from './services/ocr/OcrService'
import OvmsManager from './services/OvmsManager'
import powerMonitorService from './services/PowerMonitorService'
import { proxyManager } from './services/ProxyManager'
import { pythonService } from './services/PythonService'
import { FileServiceManager } from './services/remotefile/FileServiceManager'
@@ -115,8 +116,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const appUpdater = new AppUpdater()
const notificationService = new NotificationService()
// Initialize Python service with main window
pythonService.setMainWindow(mainWindow)
// Register shutdown handlers
powerMonitorService.registerShutdownHandler(() => {
appUpdater.setAutoUpdate(false)
})
powerMonitorService.registerShutdownHandler(() => {
const mw = windowService.getMainWindow()
if (mw && !mw.isDestroyed()) {
mw.webContents.send(IpcChannel.App_SaveData)
}
})
const checkMainWindow = () => {
if (!mainWindow || mainWindow.isDestroyed()) {

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

@@ -0,0 +1,112 @@
import { loggerService } from '@logger'
import { isLinux, isMac, isWin } from '@main/constant'
import ElectronShutdownHandler from '@paymoapp/electron-shutdown-handler'
import { BrowserWindow } from 'electron'
import { powerMonitor } from 'electron'
const logger = loggerService.withContext('PowerMonitorService')
type ShutdownHandler = () => void | Promise<void>
export class PowerMonitorService {
private static instance: PowerMonitorService
private initialized = false
private shutdownHandlers: ShutdownHandler[] = []
private constructor() {
// Private constructor to prevent direct instantiation
}
public static getInstance(): PowerMonitorService {
if (!PowerMonitorService.instance) {
PowerMonitorService.instance = new PowerMonitorService()
}
return PowerMonitorService.instance
}
/**
* Register a shutdown handler to be called when system shutdown is detected
* @param handler - The handler function to be called on shutdown
*/
public registerShutdownHandler(handler: ShutdownHandler): void {
this.shutdownHandlers.push(handler)
logger.info('Shutdown handler registered', { totalHandlers: this.shutdownHandlers.length })
}
/**
* Initialize power monitor to listen for shutdown events
*/
public init(): void {
if (this.initialized) {
logger.warn('PowerMonitorService already initialized')
return
}
if (isWin) {
this.initWindowsShutdownHandler()
} else if (isMac || isLinux) {
this.initElectronPowerMonitor()
}
this.initialized = true
logger.info('PowerMonitorService initialized', { platform: process.platform })
}
/**
* Execute all registered shutdown handlers
*/
private async executeShutdownHandlers(): Promise<void> {
logger.info('Executing shutdown handlers', { count: this.shutdownHandlers.length })
for (const handler of this.shutdownHandlers) {
try {
await handler()
} catch (error) {
logger.error('Error executing shutdown handler', error as Error)
}
}
}
/**
* Initialize shutdown handler for Windows using @paymoapp/electron-shutdown-handler
*/
private initWindowsShutdownHandler(): void {
try {
const zeroMemoryWindow = new BrowserWindow({ show: false })
// Set the window handle for the shutdown handler
ElectronShutdownHandler.setWindowHandle(zeroMemoryWindow.getNativeWindowHandle())
// Listen for shutdown event
ElectronShutdownHandler.on('shutdown', async () => {
logger.info('System shutdown event detected (Windows)')
// Execute all registered shutdown handlers
await this.executeShutdownHandlers()
// Release the shutdown block to allow the system to shut down
ElectronShutdownHandler.releaseShutdown()
})
logger.info('Windows shutdown handler registered')
} catch (error) {
logger.error('Failed to initialize Windows shutdown handler', error as Error)
}
}
/**
* Initialize power monitor for macOS and Linux using Electron's powerMonitor
*/
private initElectronPowerMonitor(): void {
try {
powerMonitor.on('shutdown', async () => {
logger.info('System shutdown event detected', { platform: process.platform })
// Execute all registered shutdown handlers
await this.executeShutdownHandlers()
})
logger.info('Electron powerMonitor shutdown listener registered')
} catch (error) {
logger.error('Failed to initialize Electron powerMonitor', error as Error)
}
}
}
// Default export as singleton instance
export default PowerMonitorService.getInstance()

View File

@@ -1,8 +1,9 @@
import { randomUUID } from 'node:crypto'
import type { BrowserWindow } from 'electron'
import { ipcMain } from 'electron'
import { windowService } from './WindowService'
interface PythonExecutionRequest {
id: string
script: string
@@ -21,7 +22,6 @@ interface PythonExecutionResponse {
*/
export class PythonService {
private static instance: PythonService | null = null
private mainWindow: BrowserWindow | null = null
private pendingRequests = new Map<string, { resolve: (value: string) => void; reject: (error: Error) => void }>()
private constructor() {
@@ -51,10 +51,6 @@ export class PythonService {
})
}
public setMainWindow(mainWindow: BrowserWindow) {
this.mainWindow = mainWindow
}
/**
* Execute Python code by sending request to renderer PyodideService
*/
@@ -63,8 +59,8 @@ export class PythonService {
context: Record<string, any> = {},
timeout: number = 60000
): Promise<string> {
if (!this.mainWindow) {
throw new Error('Main window not set in PythonService')
if (!windowService.getMainWindow()) {
throw new Error('Main window not found')
}
return new Promise((resolve, reject) => {
@@ -95,7 +91,7 @@ export class PythonService {
// Send request to renderer
const request: PythonExecutionRequest = { id: requestId, script, context, timeout }
this.mainWindow?.webContents.send('python-execution-request', request)
windowService.getMainWindow()?.webContents.send('python-execution-request', request)
})
}
}

View File

@@ -0,0 +1,285 @@
import { loggerService } from '@logger'
import { app } from 'electron'
import fs from 'fs'
import path from 'path'
const logger = loggerService.withContext('VersionService')
type OS = 'win' | 'mac' | 'linux' | 'unknown'
type Environment = 'prod' | 'dev'
type Packaged = 'packaged' | 'unpackaged'
type Mode = 'install' | 'portable'
/**
* Version record stored in version.log
*/
interface VersionRecord {
version: string
os: OS
environment: Environment
packaged: Packaged
mode: Mode
timestamp: string
}
/**
* Service for tracking application version history
* Stores version information in userData/version.log for data migration and diagnostics
*/
class VersionService {
private readonly VERSION_LOG_FILE = 'version.log'
private versionLogPath: string | null = null
constructor() {
// Lazy initialization of path since app.getPath may not be available during construction
}
/**
* Gets the full path to version.log file
* @returns {string} Full path to version log file
*/
private getVersionLogPath(): string {
if (!this.versionLogPath) {
this.versionLogPath = path.join(app.getPath('userData'), this.VERSION_LOG_FILE)
}
return this.versionLogPath
}
/**
* Gets current operating system identifier
* @returns {OS} OS identifier
*/
private getCurrentOS(): OS {
switch (process.platform) {
case 'win32':
return 'win'
case 'darwin':
return 'mac'
case 'linux':
return 'linux'
default:
return 'unknown'
}
}
/**
* Gets current environment (production or development)
* @returns {Environment} Environment identifier
*/
private getCurrentEnvironment(): Environment {
return import.meta.env.MODE === 'production' ? 'prod' : 'dev'
}
/**
* Gets packaging status
* @returns {Packaged} Packaging status
*/
private getPackagedStatus(): Packaged {
return app.isPackaged ? 'packaged' : 'unpackaged'
}
/**
* Gets installation mode (install or portable)
* @returns {Mode} Installation mode
*/
private getInstallMode(): Mode {
return process.env.PORTABLE_EXECUTABLE_DIR !== undefined ? 'portable' : 'install'
}
/**
* Generates version log line for current application state
* @returns {string} Pipe-separated version record line
*/
private generateCurrentVersionLine(): string {
const version = app.getVersion()
const os = this.getCurrentOS()
const environment = this.getCurrentEnvironment()
const packaged = this.getPackagedStatus()
const mode = this.getInstallMode()
const timestamp = new Date().toISOString()
return `${version}|${os}|${environment}|${packaged}|${mode}|${timestamp}`
}
/**
* Parses a version log line into a VersionRecord object
* @param {string} line - Pipe-separated version record line
* @returns {VersionRecord | null} Parsed version record or null if invalid
*/
private parseVersionLine(line: string): VersionRecord | null {
try {
const parts = line.trim().split('|')
if (parts.length !== 6) {
return null
}
const [version, os, environment, packaged, mode, timestamp] = parts
// Validate data
if (
!version ||
!['win', 'mac', 'linux', 'unknown'].includes(os) ||
!['prod', 'dev'].includes(environment) ||
!['packaged', 'unpackaged'].includes(packaged) ||
!['install', 'portable'].includes(mode) ||
!timestamp
) {
return null
}
return {
version,
os: os as OS,
environment: environment as Environment,
packaged: packaged as Packaged,
mode: mode as Mode,
timestamp
}
} catch (error) {
logger.warn(`Failed to parse version line: ${line}`, error as Error)
return null
}
}
/**
* Reads the last 1KB from version.log and returns all lines
* Uses reverse reading from file end to avoid reading the entire file
* @returns {string[]} Array of version lines from the last 1KB
*/
private readLastVersionLines(): string[] {
const logPath = this.getVersionLogPath()
try {
if (!fs.existsSync(logPath)) {
return []
}
const stats = fs.statSync(logPath)
const fileSize = stats.size
if (fileSize === 0) {
return []
}
// Read from the end of the file, 1KB is enough to find previous version
// Typical line: "1.7.0-beta.3|win|prod|packaged|install|2025-01-15T08:30:00.000Z\n" (~70 bytes)
// 1KB can store ~14 lines, which is more than enough
const bufferSize = Math.min(1024, fileSize)
const buffer = Buffer.alloc(bufferSize)
const fd = fs.openSync(logPath, 'r')
try {
const startPosition = Math.max(0, fileSize - bufferSize)
fs.readSync(fd, buffer, 0, bufferSize, startPosition)
const content = buffer.toString('utf-8')
const lines = content
.trim()
.split('\n')
.filter((line) => line.trim())
return lines
} finally {
fs.closeSync(fd)
}
} catch (error) {
logger.error('Failed to read version log:', error as Error)
return []
}
}
/**
* Appends a version record line to version.log
* @param {string} line - Version record line to append
*/
private appendVersionLine(line: string): void {
const logPath = this.getVersionLogPath()
try {
fs.appendFileSync(logPath, line + '\n', 'utf-8')
logger.debug(`Version recorded: ${line}`)
} catch (error) {
logger.error('Failed to append version log:', error as Error)
}
}
/**
* Records the current version on application startup
* Only adds a new record if the version has changed since the last run
*/
recordCurrentVersion(): void {
try {
const currentLine = this.generateCurrentVersionLine()
const lines = this.readLastVersionLines()
// Add new record if this is the first run or version has changed
if (lines.length === 0) {
logger.info('First run detected, creating version log')
this.appendVersionLine(currentLine)
return
}
const lastLine = lines[lines.length - 1]
const lastRecord = this.parseVersionLine(lastLine)
const currentVersion = app.getVersion()
// Check if any meaningful field has changed (version, os, environment, packaged, mode)
const currentOS = this.getCurrentOS()
const currentEnvironment = this.getCurrentEnvironment()
const currentPackaged = this.getPackagedStatus()
const currentMode = this.getInstallMode()
const hasMeaningfulChange =
!lastRecord ||
lastRecord.version !== currentVersion ||
lastRecord.os !== currentOS ||
lastRecord.environment !== currentEnvironment ||
lastRecord.packaged !== currentPackaged ||
lastRecord.mode !== currentMode
if (hasMeaningfulChange) {
logger.info(`Version information changed, recording new entry`)
this.appendVersionLine(currentLine)
} else {
logger.debug(`Version information not changed, skip recording`)
}
} catch (error) {
logger.error('Failed to record current version:', error as Error)
}
}
/**
* Gets the previous version record (last record with different version than current)
* Reads from the last 1KB of version.log to find the most recent different version
* Useful for detecting version upgrades and running migrations
* @returns {VersionRecord | null} Previous version record or null if not available
*/
getPreviousVersion(): VersionRecord | null {
try {
const lines = this.readLastVersionLines()
if (lines.length === 0) {
return null
}
const currentVersion = app.getVersion()
// Read from the end backwards to find the first different version
for (let i = lines.length - 1; i >= 0; i--) {
const record = this.parseVersionLine(lines[i])
if (record && record.version !== currentVersion) {
return record
}
}
return null
} catch (error) {
logger.error('Failed to get previous version:', error as Error)
return null
}
}
}
/**
* Singleton instance of VersionService
*/
export const versionService = new VersionService()

View File

@@ -1,4 +1,5 @@
import { loggerService } from '@logger'
import type { WebSocketCandidatesResponse, WebSocketStatusResponse } from '@shared/config/types'
import * as fs from 'fs'
import { networkInterfaces } from 'os'
import * as path from 'path'
@@ -202,12 +203,7 @@ class WebSocketService {
}
}
public getStatus = async (): Promise<{
isRunning: boolean
port?: number
ip?: string
clientConnected: boolean
}> => {
public getStatus = async (): Promise<WebSocketStatusResponse> => {
return {
isRunning: this.isStarted,
port: this.isStarted ? this.port : undefined,
@@ -216,13 +212,7 @@ class WebSocketService {
}
}
public getAllCandidates = async (): Promise<
Array<{
host: string
interface: string
priority: number
}>
> => {
public getAllCandidates = async (): Promise<WebSocketCandidatesResponse[]> => {
const interfaces = networkInterfaces()
// 按优先级排序的网络接口名称模式

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

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

@@ -1,6 +1,7 @@
import { BedrockClient, ListFoundationModelsCommand, ListInferenceProfilesCommand } from '@aws-sdk/client-bedrock'
import {
BedrockRuntimeClient,
type BedrockRuntimeClientConfig,
ConverseCommand,
InvokeModelCommand,
InvokeModelWithResponseStreamCommand
@@ -11,6 +12,8 @@ import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
import { findTokenLimit, isReasoningModel } from '@renderer/config/models'
import {
getAwsBedrockAccessKeyId,
getAwsBedrockApiKey,
getAwsBedrockAuthType,
getAwsBedrockRegion,
getAwsBedrockSecretAccessKey
} from '@renderer/hooks/useAwsBedrock'
@@ -75,32 +78,48 @@ export class AwsBedrockAPIClient extends BaseApiClient<
}
const region = getAwsBedrockRegion()
const accessKeyId = getAwsBedrockAccessKeyId()
const secretAccessKey = getAwsBedrockSecretAccessKey()
const authType = getAwsBedrockAuthType()
if (!region) {
throw new Error('AWS region is required. Please configure AWS-Region in extra headers.')
throw new Error('AWS region is required. Please configure AWS region in settings.')
}
if (!accessKeyId || !secretAccessKey) {
throw new Error('AWS credentials are required. Please configure AWS-Access-Key-ID and AWS-Secret-Access-Key.')
// Build client configuration based on auth type
let clientConfig: BedrockRuntimeClientConfig
if (authType === 'iam') {
// IAM credentials authentication
const accessKeyId = getAwsBedrockAccessKeyId()
const secretAccessKey = getAwsBedrockSecretAccessKey()
if (!accessKeyId || !secretAccessKey) {
throw new Error('AWS credentials are required. Please configure Access Key ID and Secret Access Key.')
}
clientConfig = {
region,
credentials: {
accessKeyId,
secretAccessKey
}
}
} else {
// API Key authentication
const awsBedrockApiKey = getAwsBedrockApiKey()
if (!awsBedrockApiKey) {
throw new Error('AWS Bedrock API Key is required. Please configure API Key in settings.')
}
clientConfig = {
region,
token: { token: awsBedrockApiKey },
authSchemePreference: ['httpBearerAuth']
}
}
const client = new BedrockRuntimeClient({
region,
credentials: {
accessKeyId,
secretAccessKey
}
})
const bedrockClient = new BedrockClient({
region,
credentials: {
accessKeyId,
secretAccessKey
}
})
const client = new BedrockRuntimeClient(clientConfig)
const bedrockClient = new BedrockClient(clientConfig)
this.sdkInstance = { client, bedrockClient, region }
return this.sdkInstance

View File

@@ -192,7 +192,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
extra_body: {
google: {
thinking_config: {
thinkingBudget: 0
thinking_budget: 0
}
}
}
@@ -327,8 +327,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
extra_body: {
google: {
thinking_config: {
thinkingBudget: -1,
includeThoughts: true
thinking_budget: -1,
include_thoughts: true
}
}
}
@@ -338,8 +338,8 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
extra_body: {
google: {
thinking_config: {
thinkingBudget: budgetTokens,
includeThoughts: true
thinking_budget: budgetTokens,
include_thoughts: true
}
}
}
@@ -670,7 +670,7 @@ export class OpenAIAPIClient extends OpenAIBaseClient<
} else if (isClaudeReasoningModel(model) && reasoningEffort.thinking?.budget_tokens) {
suffix = ` --thinking_budget ${reasoningEffort.thinking.budget_tokens}`
} else if (isGeminiReasoningModel(model) && reasoningEffort.extra_body?.google?.thinking_config) {
suffix = ` --thinking_budget ${reasoningEffort.extra_body.google.thinking_config.thinkingBudget}`
suffix = ` --thinking_budget ${reasoningEffort.extra_body.google.thinking_config.thinking_budget}`
}
// FIXME: poe 不支持多个text part上传文本文件的时候用的不是file part而是text part因此会出问题
// 临时解决方案是强制poe用string content但是其实poe部分支持array

View File

@@ -85,6 +85,19 @@ export function supportsLargeFileUpload(model: Model): boolean {
})
}
/**
* 检查模型是否支持TopP
*/
export function supportsTopP(model: Model): boolean {
const provider = getProviderByModel(model)
if (provider?.type === 'anthropic' || model?.endpoint_type === 'anthropic') {
return false
}
return true
}
/**
* 获取提供商特定的文件大小限制
*/

View File

@@ -34,6 +34,7 @@ import { setupToolsConfig } from '../utils/mcp'
import { buildProviderOptions } from '../utils/options'
import { getAnthropicThinkingBudget } from '../utils/reasoning'
import { buildProviderBuiltinWebSearchConfig } from '../utils/websearch'
import { supportsTopP } from './modelCapabilities'
import { getTemperature, getTopP } from './modelParameters'
const logger = loggerService.withContext('parameterBuilder')
@@ -176,20 +177,27 @@ export async function buildStreamTextParams(
messages: sdkMessages,
maxOutputTokens: maxTokens,
temperature: getTemperature(assistant, model),
topP: getTopP(assistant, model),
abortSignal: options.requestOptions?.signal,
headers: options.requestOptions?.headers,
providerOptions,
stopWhen: stepCountIs(20),
maxRetries: 0
}
if (supportsTopP(model)) {
params.topP = getTopP(assistant, model)
}
if (tools) {
params.tools = tools
}
if (assistant.prompt) {
params.system = await replacePromptVariables(assistant.prompt, model.name)
}
logger.debug('params', params)
return {
params,
modelId: model.id,

View File

@@ -21,10 +21,45 @@ vi.mock('@renderer/store', () => ({
}
}))
vi.mock('@renderer/utils/api', () => ({
formatApiHost: vi.fn((host, isSupportedAPIVersion = true) => {
if (isSupportedAPIVersion === false) {
return host // Return host as-is when isSupportedAPIVersion is false
}
return `${host}/v1` // Default behavior when isSupportedAPIVersion is true
}),
routeToEndpoint: vi.fn((host) => ({
baseURL: host,
endpoint: '/chat/completions'
}))
}))
vi.mock('@renderer/config/providers', async (importOriginal) => {
const actual = (await importOriginal()) as any
return {
...actual,
isCherryAIProvider: vi.fn(),
isPerplexityProvider: vi.fn(),
isAnthropicProvider: vi.fn(() => false),
isAzureOpenAIProvider: vi.fn(() => false),
isGeminiProvider: vi.fn(() => false),
isNewApiProvider: vi.fn(() => false)
}
})
vi.mock('@renderer/hooks/useVertexAI', () => ({
isVertexProvider: vi.fn(() => false),
isVertexAIConfigured: vi.fn(() => false),
createVertexProvider: vi.fn()
}))
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'
import { COPILOT_DEFAULT_HEADERS, COPILOT_EDITOR_VERSION, isCopilotResponsesModel } from '../constants'
import { providerToAiSdkConfig } from '../providerConfig'
import { getActualProvider, providerToAiSdkConfig } from '../providerConfig'
const createWindowKeyv = () => {
const store = new Map<string, string>()
@@ -46,11 +81,31 @@ const createCopilotProvider = (): Provider => ({
isSystem: true
})
const createModel = (id: string, name = id): Model => ({
const createModel = (id: string, name = id, provider = 'copilot'): Model => ({
id,
name,
provider: 'copilot',
group: 'copilot'
provider,
group: provider
})
const createCherryAIProvider = (): Provider => ({
id: 'cherryai',
type: 'openai',
name: 'CherryAI',
apiKey: 'test-key',
apiHost: 'https://api.cherryai.com',
models: [],
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', () => {
@@ -87,3 +142,134 @@ describe('Copilot responses routing', () => {
expect(config.options.headers?.['Copilot-Integration-Id']).toBe(COPILOT_DEFAULT_HEADERS['Copilot-Integration-Id'])
})
})
describe('CherryAI provider configuration', () => {
beforeEach(() => {
;(globalThis as any).window = {
...(globalThis as any).window,
keyv: createWindowKeyv()
}
vi.clearAllMocks()
})
it('formats CherryAI provider apiHost with false parameter', () => {
const provider = createCherryAIProvider()
const model = createModel('gpt-4', 'GPT-4', 'cherryai')
// Mock the functions to simulate CherryAI provider detection
vi.mocked(isCherryAIProvider).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.cherryai.com', false)
expect(actualProvider.apiHost).toBe('https://api.cherryai.com')
})
it('does not format non-CherryAI 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-CherryAI provider
vi.mocked(isCherryAIProvider).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 CherryAI provider with empty apiHost', () => {
const provider = createCherryAIProvider()
provider.apiHost = ''
const model = createModel('gpt-4', 'GPT-4', 'cherryai')
vi.mocked(isCherryAIProvider).mockReturnValue(true)
vi.mocked(getProviderByModel).mockReturnValue(provider)
const actualProvider = getActualProvider(model)
expect(formatApiHost).toHaveBeenCalledWith('', false)
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

@@ -52,7 +52,7 @@ const AIHUBMIX_RULES: RuleSet = {
}
}
],
fallbackRule: (provider: Provider) => provider
fallbackRule: (provider: Provider) => extraProviderConfig(provider)
}
export const aihubmixProviderCreator = provider2Provider.bind(null, AIHUBMIX_RULES)

View File

@@ -9,11 +9,15 @@ import { isOpenAIChatCompletionOnlyModel } from '@renderer/config/models'
import {
isAnthropicProvider,
isAzureOpenAIProvider,
isCherryAIProvider,
isGeminiProvider,
isNewApiProvider
isNewApiProvider,
isPerplexityProvider
} from '@renderer/config/providers'
import {
getAwsBedrockAccessKeyId,
getAwsBedrockApiKey,
getAwsBedrockAuthType,
getAwsBedrockRegion,
getAwsBedrockSecretAccessKey
} from '@renderer/hooks/useAwsBedrock'
@@ -98,6 +102,10 @@ function formatProviderApiHost(provider: Provider): Provider {
formatted.apiHost = formatAzureOpenAIApiHost(formatted.apiHost)
} else if (isVertexProvider(formatted)) {
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)
}
@@ -192,9 +200,15 @@ export function providerToAiSdkConfig(
// bedrock
if (aiSdkProviderId === 'bedrock') {
const authType = getAwsBedrockAuthType()
extraOptions.region = getAwsBedrockRegion()
extraOptions.accessKeyId = getAwsBedrockAccessKeyId()
extraOptions.secretAccessKey = getAwsBedrockSecretAccessKey()
if (authType === 'apiKey') {
extraOptions.apiKey = getAwsBedrockApiKey()
} else {
extraOptions.accessKeyId = getAwsBedrockAccessKeyId()
extraOptions.secretAccessKey = getAwsBedrockSecretAccessKey()
}
}
// google-vertex
if (aiSdkProviderId === 'google-vertex' || aiSdkProviderId === 'google-vertex-anthropic') {

View File

@@ -17,6 +17,7 @@ import { getAiSdkProviderId } from '../provider/factory'
import { buildGeminiGenerateImageParams } from './image'
import {
getAnthropicReasoningParams,
getBedrockReasoningParams,
getCustomParameters,
getGeminiReasoningParams,
getOpenAIReasoningParams,
@@ -127,6 +128,9 @@ export function buildProviderOptions(
case 'google-vertex-anthropic':
providerSpecificOptions = buildAnthropicProviderOptions(assistant, model, capabilities)
break
case 'bedrock':
providerSpecificOptions = buildBedrockProviderOptions(assistant, model, capabilities)
break
default:
// 对于其他 provider使用通用的构建逻辑
providerSpecificOptions = {
@@ -266,6 +270,32 @@ function buildXAIProviderOptions(
return providerOptions
}
/**
* Build Bedrock providerOptions
*/
function buildBedrockProviderOptions(
assistant: Assistant,
model: Model,
capabilities: {
enableReasoning: boolean
enableWebSearch: boolean
enableGenerateImage: boolean
}
): Record<string, any> {
const { enableReasoning } = capabilities
let providerOptions: Record<string, any> = {}
if (enableReasoning) {
const reasoningParams = getBedrockReasoningParams(assistant, model)
providerOptions = {
...providerOptions,
...reasoningParams
}
}
return providerOptions
}
/**
* 构建通用的 providerOptions用于其他 provider
*/

View File

@@ -98,7 +98,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
extra_body: {
google: {
thinking_config: {
thinkingBudget: 0
thinking_budget: 0
}
}
}
@@ -259,8 +259,8 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
extra_body: {
google: {
thinking_config: {
thinkingBudget: -1,
includeThoughts: true
thinking_budget: -1,
include_thoughts: true
}
}
}
@@ -270,8 +270,8 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
extra_body: {
google: {
thinking_config: {
thinkingBudget: budgetTokens,
includeThoughts: true
thinking_budget: budgetTokens ?? -1,
include_thoughts: true
}
}
}
@@ -431,8 +431,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
if (reasoningEffort === undefined) {
return {
thinkingConfig: {
includeThoughts: false,
...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinkingBudget: 0 } : {})
include_thoughts: false,
...(GEMINI_FLASH_MODEL_REGEX.test(model.id) ? { thinking_budget: 0 } : {})
}
}
}
@@ -442,7 +442,7 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
if (effortRatio > 1) {
return {
thinkingConfig: {
includeThoughts: true
include_thoughts: true
}
}
}
@@ -452,8 +452,8 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
return {
thinkingConfig: {
...(budget > 0 ? { thinkingBudget: budget } : {}),
includeThoughts: true
...(budget > 0 ? { thinking_budget: budget } : {}),
include_thoughts: true
}
}
}
@@ -485,6 +485,34 @@ export function getXAIReasoningParams(assistant: Assistant, model: Model): Recor
}
}
/**
* Get Bedrock reasoning parameters
*/
export function getBedrockReasoningParams(assistant: Assistant, model: Model): Record<string, any> {
if (!isReasoningModel(model)) {
return {}
}
const reasoningEffort = assistant?.settings?.reasoning_effort
if (reasoningEffort === undefined) {
return {}
}
// Only apply thinking budget for Claude reasoning models
if (!isSupportedThinkingTokenClaudeModel(model)) {
return {}
}
const budgetTokens = getAnthropicThinkingBudget(assistant, model)
return {
reasoningConfig: {
type: 'enabled',
budgetTokens: budgetTokens
}
}
}
/**
* 获取自定义参数
* 从 assistant 设置中提取自定义参数

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="33" height="38" viewBox="0 0 33 38" fill="none">
<g clip-path="url(#clip0_4321_9943)">
<path d="M1.51221 6.59813C1.51221 4.09263 3.54331 2.06152 6.04881 2.06152H27.9757C30.4812 2.06152 32.5123 4.09263 32.5123 6.59813C32.5123 9.10362 30.4812 11.1347 27.9757 11.1347H6.04881C3.54331 11.1347 1.51221 9.10362 1.51221 6.59813Z" fill="#6200EE"/>
<path d="M3.38905 3.56467C5.26076 1.89906 8.12831 2.06615 9.79391 3.93785L22.1493 17.8221C23.8149 19.6938 23.6478 22.5614 21.7761 24.227C19.9044 25.8926 17.0369 25.7255 15.3713 23.8538L3.01586 9.96953C1.35026 8.09782 1.51734 5.23027 3.38905 3.56467Z" fill="#6200EE"/>
<path d="M1.51221 20.9643C1.51221 18.4588 3.54331 16.4277 6.04881 16.4277H18.9025C21.408 16.4277 23.4391 18.4588 23.4391 20.9643C23.4391 23.4698 21.408 25.5009 18.9025 25.5009H6.04881C3.54331 25.5009 1.51221 23.4698 1.51221 20.9643Z" fill="#6200EE"/>
<path d="M10.5854 32.3052C10.5854 34.8107 8.55431 36.8418 6.04881 36.8418C3.54331 36.8418 1.51221 34.8107 1.51221 32.3052C1.51221 29.7997 3.54331 27.7686 6.04881 27.7686C8.55431 27.7686 10.5854 29.7997 10.5854 32.3052Z" fill="#BF7AFF"/>
</g>
<defs>
<clipPath id="clip0_4321_9943">
<rect width="32.5124" height="36.9029" fill="white" transform="translate(0 0.548828)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 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,9 +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'
@@ -23,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>
@@ -38,10 +37,10 @@ const ScanQRCode: React.FC<{ qrCodeValue: string }> = ({ qrCodeValue }) => {
<QRCodeSVG
marginSize={2}
value={qrCodeValue}
level="Q"
size={160}
level="H"
size={200}
imageSettings={{
src: '/src/assets/images/logo.png',
src: AppLogo,
width: 40,
height: 40,
excavate: true
@@ -70,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>
@@ -135,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()
@@ -198,17 +196,28 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const { port, ip } = await window.api.webSocket.status()
if (ip && port) {
const candidates = await window.api.webSocket.getAllCandidates()
const connectionInfo = {
type: 'cherry-studio-app',
candidates,
selectedHost: ip,
port,
timestamp: Date.now()
const candidatesData = await window.api.webSocket.getAllCandidates()
const optimizeConnectionInfo = () => {
const ipToNumber = (ip: string) => {
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0)
}
const compressedData = [
'CSA',
ipToNumber(ip),
candidatesData.map((candidate: WebSocketCandidatesResponse) => ipToNumber(candidate.host)),
port, // 端口号
Date.now() % 86400000
]
return compressedData
}
setQrCodeValue(JSON.stringify(connectionInfo))
const compressedData = optimizeConnectionInfo()
const qrCodeValue = JSON.stringify(compressedData)
setQrCodeValue(qrCodeValue)
setConnectionPhase('waiting_qr_scan')
logger.info(`QR code generated: ${ip}:${port} with ${candidates.length} IP candidates`)
} else {
setError(t('settings.data.export_to_phone.lan.error.no_ip'))
setConnectionPhase('error')
@@ -286,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 () => {
@@ -363,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>
@@ -399,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',
@@ -428,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>
@@ -475,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

@@ -64,6 +64,7 @@ export type QuickPanelListItem = {
isSelected?: boolean
isMenu?: boolean
disabled?: boolean
hidden?: boolean
/**
* 固定显示项:不参与过滤,始终出现在列表顶部。
* 例如“清除”按钮可设置为 alwaysVisible从而在有匹配项时始终可见

View File

@@ -143,7 +143,8 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
prevSymbolRef.current = ctx.symbol
// 固定项置顶 + 过滤后的普通项
return [...pinnedItems, ...filteredNormalItems]
const pinnedFiltered = [...pinnedItems, ...filteredNormalItems]
return pinnedFiltered.filter((item) => !item.hidden)
}, [ctx.isVisible, ctx.symbol, ctx.list, searchText])
const canForwardAndBackward = useMemo(() => {

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

@@ -27,6 +27,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
],
cherryin: [],
vertexai: [],
sophnet: [],
'302ai': [
{
id: 'deepseek-chat',

View File

@@ -46,6 +46,7 @@ import Ph8ProviderLogo from '@renderer/assets/images/providers/ph8.png'
import PPIOProviderLogo from '@renderer/assets/images/providers/ppio.png'
import QiniuProviderLogo from '@renderer/assets/images/providers/qiniu.webp'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import SophnetProviderLogo from '@renderer/assets/images/providers/sophnet.svg'
import StepProviderLogo from '@renderer/assets/images/providers/step.png'
import TencentCloudProviderLogo from '@renderer/assets/images/providers/tencent-cloud-ti.png'
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
@@ -246,6 +247,16 @@ export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> =
isSystem: true,
enabled: false
},
sophnet: {
id: 'sophnet',
name: 'SophNet',
type: 'openai',
apiKey: '',
apiHost: 'https://www.sophnet.com/api/open-apis/v1',
models: [],
isSystem: true,
enabled: false
},
ppio: {
id: 'ppio',
name: 'PPIO',
@@ -729,7 +740,8 @@ export const PROVIDER_LOGO_MAP: AtLeast<SystemProviderId, string> = {
poe: 'poe', // use svg icon component
aionly: AiOnlyProviderLogo,
longcat: LongCatProviderLogo,
huggingface: HuggingfaceProviderLogo
huggingface: HuggingfaceProviderLogo,
sophnet: SophnetProviderLogo
} as const
export function getProviderLogo(providerId: string) {
@@ -808,6 +820,17 @@ export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
models: 'https://ai.burncloud.com/pricing'
}
},
sophnet: {
api: {
url: 'https://www.sophnet.com/api/open-apis/v1'
},
websites: {
official: 'https://sophnet.com',
apiKey: 'https://sophnet.com/#/project/key',
docs: 'https://sophnet.com/docs/component/introduce.html',
models: 'https://sophnet.com/#/model/list'
}
},
ppio: {
api: {
url: 'https://api.ppinfra.com/v3/openai'
@@ -1463,6 +1486,14 @@ export const isNewApiProvider = (provider: Provider) => {
return ['new-api', 'cherryin'].includes(provider.id) || provider.type === 'new-api'
}
export function isCherryAIProvider(provider: Provider): boolean {
return provider.id === 'cherryai'
}
export function isPerplexityProvider(provider: Provider): boolean {
return provider.id === 'perplexity'
}
/**
* 判断是否为 OpenAI 兼容的提供商
* @param {Provider} provider 提供商对象
@@ -1488,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

@@ -1,5 +1,12 @@
import store, { useAppSelector } from '@renderer/store'
import { setAwsBedrockAccessKeyId, setAwsBedrockRegion, setAwsBedrockSecretAccessKey } from '@renderer/store/llm'
import {
setAwsBedrockAccessKeyId,
setAwsBedrockApiKey,
setAwsBedrockAuthType,
setAwsBedrockRegion,
setAwsBedrockSecretAccessKey
} from '@renderer/store/llm'
import type { AwsBedrockAuthType } from '@renderer/types'
import { useDispatch } from 'react-redux'
export function useAwsBedrockSettings() {
@@ -8,8 +15,10 @@ export function useAwsBedrockSettings() {
return {
...settings,
setAuthType: (authType: AwsBedrockAuthType) => dispatch(setAwsBedrockAuthType(authType)),
setAccessKeyId: (accessKeyId: string) => dispatch(setAwsBedrockAccessKeyId(accessKeyId)),
setSecretAccessKey: (secretAccessKey: string) => dispatch(setAwsBedrockSecretAccessKey(secretAccessKey)),
setApiKey: (apiKey: string) => dispatch(setAwsBedrockApiKey(apiKey)),
setRegion: (region: string) => dispatch(setAwsBedrockRegion(region))
}
}
@@ -18,6 +27,10 @@ export function getAwsBedrockSettings() {
return store.getState().llm.settings.awsBedrock
}
export function getAwsBedrockAuthType() {
return store.getState().llm.settings.awsBedrock.authType
}
export function getAwsBedrockAccessKeyId() {
return store.getState().llm.settings.awsBedrock.accessKeyId
}
@@ -26,6 +39,10 @@ export function getAwsBedrockSecretAccessKey() {
return store.getState().llm.settings.awsBedrock.secretAccessKey
}
export function getAwsBedrockApiKey() {
return store.getState().llm.settings.awsBedrock.apiKey
}
export function getAwsBedrockRegion() {
return store.getState().llm.settings.awsBedrock.region
}

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

@@ -85,7 +85,8 @@ const providerKeyMap = {
poe: 'provider.poe',
aionly: 'provider.aionly',
longcat: 'provider.longcat',
huggingface: 'provider.huggingface'
huggingface: 'provider.huggingface',
sophnet: 'provider.sophnet'
} as const
/**
@@ -238,7 +239,7 @@ const paintingsImageSizeOptionsKeyMap = {
} as const
export const getPaintingsImageSizeOptionsLabel = (key: string): string => {
return getLabel(paintingsImageSizeOptionsKeyMap, key)
return paintingsImageSizeOptionsKeyMap[key] ? getLabel(paintingsImageSizeOptionsKeyMap, key) : key
}
const paintingsQualityOptionsKeyMap = {

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": {
@@ -2482,6 +2517,7 @@
"qiniu": "Qiniu AI",
"qwenlm": "QwenLM",
"silicon": "SiliconFlow",
"sophnet": "SophNet",
"stepfun": "StepFun",
"tencent-cloud-ti": "Tencent Cloud TI",
"together": "Together",
@@ -4259,6 +4295,12 @@
"aws-bedrock": {
"access_key_id": "AWS Access Key ID",
"access_key_id_help": "Your AWS Access Key ID for accessing AWS Bedrock services",
"api_key": "Bedrock API Key",
"api_key_help": "Your AWS Bedrock API Key for authentication",
"auth_type": "Authentication Type",
"auth_type_api_key": "Bedrock API Key",
"auth_type_help": "Choose between IAM credentials or Bedrock API Key authentication",
"auth_type_iam": "IAM Credentials",
"description": "AWS Bedrock is Amazon's fully managed foundation model service that supports various advanced large language models",
"region": "AWS Region",
"region_help": "Your AWS service region, e.g., 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": {
@@ -2482,6 +2517,7 @@
"qiniu": "七牛云 AI 推理",
"qwenlm": "QwenLM",
"silicon": "硅基流动",
"sophnet": "SophNet",
"stepfun": "阶跃星辰",
"tencent-cloud-ti": "腾讯云 TI",
"together": "Together",
@@ -4259,6 +4295,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 是亚马逊提供的全托管基础模型服务,支持多种先进的大语言模型",
"region": "AWS 区域",
"region_help": "您的 AWS 服务区域,例如 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": {
@@ -2482,6 +2517,7 @@
"qiniu": "七牛雲 AI 推理",
"qwenlm": "QwenLM",
"silicon": "SiliconFlow",
"sophnet": "SophNet",
"stepfun": "StepFun",
"tencent-cloud-ti": "騰訊雲 TI",
"together": "Together",
@@ -4259,6 +4295,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 是亞馬遜提供的全托管基础模型服務,支持多種先進的大語言模型",
"region": "AWS 區域",
"region_help": "您的 AWS 服務區域,例如 us-east-1",

View File

@@ -1,6 +1,7 @@
{
"agent": {
"add": {
"description": "Bewältigen Sie komplexe Aufgaben mit verschiedenen Werkzeugen",
"error": {
"failed": "Agent hinzufügen fehlgeschlagen",
"invalid_agent": "Ungültiger Agent"
@@ -547,8 +548,12 @@
"chat": {
"add": {
"assistant": {
"description": "Tägliche Gespräche und schnelle Fragen & Antworten",
"title": "Assistent hinzufügen"
},
"option": {
"title": "Typ auswählen"
},
"topic": {
"title": "Neues Thema erstellen"
}
@@ -1606,7 +1611,7 @@
},
"assistant": {
"added": {
"content": "Agent erfolgreich hinzugefügt"
"content": "Assistent erfolgreich hinzugefügt"
}
},
"attachments": {
@@ -2471,12 +2476,13 @@
"openrouter": "OpenRouter",
"ovms": "Intel OVMS",
"perplexity": "Perplexity",
"ph8": "PH8 Großmodell-Plattform",
"ph8": "PH8",
"poe": "Poe",
"ppio": "PPIO Cloud",
"qiniu": "Qiniu Cloud KI-Inferenz",
"qwenlm": "QwenLM",
"silicon": "SiliconFlow",
"sophnet": "SophNet",
"stepfun": "StepFun",
"tencent-cloud-ti": "Tencent Cloud TI",
"together": "Together",
@@ -2923,15 +2929,14 @@
},
"description": "Ein KI-Assistent für Kreative",
"downloading": "Update wird heruntergeladen...",
"enterprise": {
"title": "Unternehmen"
},
"feedback": {
"button": "Feedback",
"title": "Feedback"
},
"label": "Über uns",
"license": {
"button": "Anzeigen",
"title": "Lizenz"
},
"releases": {
"button": "Anzeigen",
"title": "Changelog"
@@ -4255,6 +4260,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

@@ -1,6 +1,7 @@
{
"agent": {
"add": {
"description": "Χειριστείτε πολύπλοκες εργασίες με διάφορα εργαλεία",
"error": {
"failed": "Αποτυχία προσθήκης πράκτορα",
"invalid_agent": "Μη έγκυρος Agent"
@@ -547,8 +548,12 @@
"chat": {
"add": {
"assistant": {
"description": "Καθημερινές συνομιλίες και γρήγορες ερωταπαντήσεις",
"title": "Προσθήκη βοηθού"
},
"option": {
"title": "Επιλέξτε Τύπο"
},
"topic": {
"title": "Δημιουργία νέου θέματος"
}
@@ -1606,7 +1611,7 @@
},
"assistant": {
"added": {
"content": "Ο ενεργοποιημένος αστρόναυτης προστέθηκε επιτυχώς"
"content": "Ο βοηθός προστέθηκε επιτυχώς"
}
},
"attachments": {
@@ -2471,12 +2476,13 @@
"openrouter": "OpenRouter",
"ovms": "Intel OVMS",
"perplexity": "Perplexity",
"ph8": "Πλατφόρμα Ανοιχτής Μεγάλης Μοντέλου PH8",
"ph8": "PH8",
"poe": "Poe",
"ppio": "PPIO Piao Yun",
"qiniu": "Qiniu AI",
"qwenlm": "QwenLM",
"silicon": "Σιδηρική Παρουσία",
"sophnet": "SophNet",
"stepfun": "Βήμα Ουράς",
"tencent-cloud-ti": "Tencent Cloud TI",
"together": "Together",
@@ -2923,15 +2929,14 @@
},
"description": "Ένα AI ασιστάντα που έχει σχεδιαστεί για δημιουργούς",
"downloading": "Λήψη ενημερώσεων...",
"enterprise": {
"title": "Επιχείρηση"
},
"feedback": {
"button": "Σχόλια και Παρατηρήσεις",
"title": "Αποστολή σχολίων"
},
"label": "Περί μας",
"license": {
"button": "Προβολή",
"title": "Licenses"
},
"releases": {
"button": "Προβολή",
"title": "Ημερολόγιο Ενημερώσεων"
@@ -4255,6 +4260,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

@@ -1,6 +1,7 @@
{
"agent": {
"add": {
"description": "Maneja tareas complejas con varias herramientas",
"error": {
"failed": "Error al añadir agente",
"invalid_agent": "Agent inválido"
@@ -547,8 +548,12 @@
"chat": {
"add": {
"assistant": {
"description": "Conversaciones diarias y preguntas y respuestas rápidas",
"title": "Agregar asistente"
},
"option": {
"title": "Seleccionar Tipo"
},
"topic": {
"title": "Crear nuevo tema"
}
@@ -2471,12 +2476,13 @@
"openrouter": "OpenRouter",
"ovms": "Intel OVMS",
"perplexity": "Perplejidad",
"ph8": "Plataforma Abierta de Grandes Modelos PH8",
"ph8": "PH8",
"poe": "Poe",
"ppio": "PPIO Cloud Piao",
"qiniu": "Qiniu AI",
"qwenlm": "QwenLM",
"silicon": "Silicio Fluido",
"sophnet": "SophNet",
"stepfun": "Función Salto",
"tencent-cloud-ti": "Tencent Nube TI",
"together": "Juntos",
@@ -2923,15 +2929,14 @@
},
"description": "Una asistente de IA creada para los creadores",
"downloading": "Descargando actualización...",
"enterprise": {
"title": "Empresa"
},
"feedback": {
"button": "Enviar feedback",
"title": "Enviar comentarios"
},
"label": "Acerca de nosotros",
"license": {
"button": "Ver",
"title": "Licencia"
},
"releases": {
"button": "Ver",
"title": "Registro de cambios"
@@ -4255,6 +4260,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

@@ -1,6 +1,7 @@
{
"agent": {
"add": {
"description": "Gérez des tâches complexes avec divers outils",
"error": {
"failed": "Échec de l'ajout de l'agent",
"invalid_agent": "Agent invalide"
@@ -547,8 +548,12 @@
"chat": {
"add": {
"assistant": {
"description": "Conversations quotidiennes et Q&R rapides",
"title": "Ajouter un assistant"
},
"option": {
"title": "Sélectionner le type"
},
"topic": {
"title": "Nouveau sujet"
}
@@ -2471,12 +2476,13 @@
"openrouter": "OpenRouter",
"ovms": "Intel OVMS",
"perplexity": "Perplexité",
"ph8": "Plateforme ouverte de grands modèles PH8",
"ph8": "PH8",
"poe": "Poe",
"ppio": "PPIO Cloud Piou",
"qiniu": "Qiniu AI",
"qwenlm": "QwenLM",
"silicon": "Silicium Fluide",
"sophnet": "SophNet",
"stepfun": "Échelon Étoile",
"tencent-cloud-ti": "Tencent Cloud TI",
"together": "Ensemble",
@@ -2923,15 +2929,14 @@
},
"description": "Un assistant IA conçu pour les créateurs",
"downloading": "Téléchargement de la mise à jour en cours...",
"enterprise": {
"title": "Entreprise"
},
"feedback": {
"button": "Faire un retour",
"title": "Retour d'information"
},
"label": "À propos de nous",
"license": {
"button": "Afficher",
"title": "Licence"
},
"releases": {
"button": "Afficher",
"title": "Journal des mises à jour"
@@ -4255,6 +4260,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

@@ -1,6 +1,7 @@
{
"agent": {
"add": {
"description": "さまざまなツールを使って複雑なタスクを処理する",
"error": {
"failed": "エージェントの追加に失敗しました",
"invalid_agent": "無効なエージェント"
@@ -547,8 +548,12 @@
"chat": {
"add": {
"assistant": {
"description": "日常会話と簡単なQ&A",
"title": "アシスタントを追加"
},
"option": {
"title": "種類を選択"
},
"topic": {
"title": "新しいトピック"
}
@@ -1606,7 +1611,7 @@
},
"assistant": {
"added": {
"content": "アシスタントが追加されました"
"content": "助手が追加されました"
}
},
"attachments": {
@@ -2477,6 +2482,7 @@
"qiniu": "七牛云 AI 推理",
"qwenlm": "QwenLM",
"silicon": "SiliconFlow",
"sophnet": "SophNet",
"stepfun": "StepFun",
"tencent-cloud-ti": "Tencent Cloud TI",
"together": "Together",
@@ -2923,15 +2929,14 @@
},
"description": "クリエイターのための強力なAIアシスタント",
"downloading": "ダウンロード中...",
"enterprise": {
"title": "エンタープライズ"
},
"feedback": {
"button": "フィードバック",
"title": "フィードバック"
},
"label": "について",
"license": {
"button": "ライセンス",
"title": "ライセンス"
},
"releases": {
"button": "リリース",
"title": "リリースノート"
@@ -4255,6 +4260,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

@@ -1,6 +1,7 @@
{
"agent": {
"add": {
"description": "Lide com tarefas complexas usando várias ferramentas",
"error": {
"failed": "Falha ao adicionar agente",
"invalid_agent": "Agent inválido"
@@ -547,8 +548,12 @@
"chat": {
"add": {
"assistant": {
"description": "Conversas diárias e perguntas e respostas rápidas",
"title": "Adicionar assistente"
},
"option": {
"title": "Selecionar Tipo"
},
"topic": {
"title": "Novo Tópico"
}
@@ -2471,12 +2476,13 @@
"openrouter": "OpenRouter",
"ovms": "Intel OVMS",
"perplexity": "Perplexidade",
"ph8": "Plataforma Aberta de Grandes Modelos PH8",
"ph8": "PH8",
"poe": "Poe",
"ppio": "PPIO Nuvem Piao",
"qiniu": "Qiniu AI",
"qwenlm": "QwenLM",
"silicon": "Silício em Fluxo",
"sophnet": "SophNet",
"stepfun": "Função de Passo Estelar",
"tencent-cloud-ti": "Nuvem TI da Tencent",
"together": "Juntos",
@@ -2923,15 +2929,14 @@
},
"description": "Um assistente de IA criado para criadores",
"downloading": "Baixando atualizações...",
"enterprise": {
"title": "Empresa"
},
"feedback": {
"button": "Feedback",
"title": "Enviar feedback"
},
"label": "Sobre Nós",
"license": {
"button": "Ver",
"title": "Licença"
},
"releases": {
"button": "Ver",
"title": "Registro de alterações"
@@ -4255,6 +4260,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

@@ -1,6 +1,7 @@
{
"agent": {
"add": {
"description": "Справляйтесь со сложными задачами с помощью различных инструментов",
"error": {
"failed": "Не удалось добавить агента",
"invalid_agent": "Недействительный агент"
@@ -547,8 +548,12 @@
"chat": {
"add": {
"assistant": {
"description": "Ежедневные разговоры и быстрые вопросы и ответы",
"title": "Добавить ассистента"
},
"option": {
"title": "Выберите тип"
},
"topic": {
"title": "Новый топик"
}
@@ -2477,6 +2482,7 @@
"qiniu": "Qiniu AI",
"qwenlm": "QwenLM",
"silicon": "SiliconFlow",
"sophnet": "SophNet",
"stepfun": "StepFun",
"tencent-cloud-ti": "Tencent Cloud TI",
"together": "Together",
@@ -2923,15 +2929,14 @@
},
"description": "Мощный AI-ассистент для созидания",
"downloading": "Загрузка...",
"enterprise": {
"title": "Предприятие"
},
"feedback": {
"button": "Обратная связь",
"title": "Обратная связь"
},
"label": "О программе и обратная связь",
"license": {
"button": "Лицензия",
"title": "Лицензия"
},
"releases": {
"button": "Релизы",
"title": "Заметки о релизах"
@@ -3043,7 +3048,7 @@
"confirm": {
"button": "Выберите файл резервной копии"
},
"content": "Экспорт части данных, включая чат и настройки. Пожалуйста, обратите внимание, что процесс резервного копирования может занять некоторое время. Благодарим за ваше терпение.",
"content": "Экспорт части данных, включая историю чатов и настройки. Обратите внимание, процесс резервного копирования может занять некоторое время, благодарим за ваше терпение.",
"lan": {
"auto_close_tip": "Автоматическое закрытие через {{seconds}} секунд...",
"confirm_close_message": "Передача файла в процессе. Закрытие прервет передачу. Вы уверены, что хотите принудительно закрыть?",
@@ -4255,6 +4260,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

@@ -2,6 +2,7 @@ import type { DropResult } from '@hello-pangea/dnd'
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
import { loggerService } from '@logger'
import { ActionIconButton } from '@renderer/components/Buttons'
import { MdiLightbulbOn } from '@renderer/components/Icons'
import type { QuickPanelListItem } from '@renderer/components/QuickPanel'
import {
isAnthropicModel,
@@ -230,6 +231,15 @@ const InputbarTools = ({
quickPhrasesButtonRef.current?.openQuickPanel()
}
},
{
label: t('assistants.settings.reasoning_effort.label'),
description: '',
icon: <MdiLightbulbOn />,
isMenu: true,
action: () => {
thinkingButtonRef.current?.openQuickPanel()
}
},
{
label: t('assistants.presets.edit.model.select.title'),
description: '',
@@ -245,6 +255,7 @@ const InputbarTools = ({
icon: <FileSearch />,
isMenu: true,
disabled: files.length > 0,
hidden: !showKnowledgeBaseButton,
action: () => {
knowledgeBaseButtonRef.current?.openQuickPanel()
}
@@ -312,7 +323,7 @@ const InputbarTools = ({
translate()
}
}
]
] satisfies QuickPanelListItem[]
}
const handleDragEnd = (result: DropResult) => {

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

Some files were not shown because too many files have changed in this diff Show More