diff --git a/CLAUDE.md b/CLAUDE.md index 2716815ba2..0728605824 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/package.json b/package.json index c4259a85a1..5c41f0df65 100644 --- a/package.json +++ b/package.json @@ -147,7 +147,6 @@ "@eslint/js": "^9.22.0", "@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch", "@hello-pangea/dnd": "^18.0.1", - "@heroui/react": "^2.8.3", "@kangfenmao/keyv-storage": "^0.1.0", "@langchain/community": "^1.0.0", "@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch", @@ -349,6 +348,7 @@ "striptags": "^3.2.0", "styled-components": "^6.1.11", "swr": "^2.3.6", + "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.13", "tar": "^7.4.3", "tiny-pinyin": "^1.3.2", diff --git a/src/main/apiServer/middleware/openapi.ts b/src/main/apiServer/middleware/openapi.ts index c136fecdde..ff01005bd9 100644 --- a/src/main/apiServer/middleware/openapi.ts +++ b/src/main/apiServer/middleware/openapi.ts @@ -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) { diff --git a/src/main/services/agents/services/claudecode/index.ts b/src/main/services/agents/services/claudecode/index.ts index c9a8c677cd..4e20520017 100644 --- a/src/main/services/agents/services/claudecode/index.ts +++ b/src/main/services/agents/services/claudecode/index.ts @@ -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') + } } } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 78396c49e7..703015e30e 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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 ( - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + ) diff --git a/src/renderer/src/assets/styles/index.css b/src/renderer/src/assets/styles/index.css index eaa984270f..ed1eb555a3 100644 --- a/src/renderer/src/assets/styles/index.css +++ b/src/renderer/src/assets/styles/index.css @@ -41,11 +41,11 @@ body, margin: 0; } -/* #root { +#root { display: flex; flex-direction: row; flex: 1; -} */ +} body { display: flex; diff --git a/src/renderer/src/assets/styles/tailwind.css b/src/renderer/src/assets/styles/tailwind.css index f05b01b65c..5cd4046689 100644 --- a/src/renderer/src/assets/styles/tailwind.css +++ b/src/renderer/src/assets/styles/tailwind.css @@ -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 { diff --git a/src/renderer/src/components/ApiModelLabel.tsx b/src/renderer/src/components/ApiModelLabel.tsx deleted file mode 100644 index 3e36083a69..0000000000 --- a/src/renderer/src/components/ApiModelLabel.tsx +++ /dev/null @@ -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, 'children'> { - model?: ApiModel - classNames?: { - container?: string - avatar?: string - modelName?: string - divider?: string - providerName?: string - } -} - -export const ApiModelLabel: React.FC = ({ model, className, classNames, ...props }) => { - return ( -
- - {model?.name} - | - {model?.provider_name} -
- ) -} diff --git a/src/renderer/src/components/Avatar/EmojiAvatarWithPicker.tsx b/src/renderer/src/components/Avatar/EmojiAvatarWithPicker.tsx index 6735d86a4e..649f619cba 100644 --- a/src/renderer/src/components/Avatar/EmojiAvatarWithPicker.tsx +++ b/src/renderer/src/components/Avatar/EmojiAvatarWithPicker.tsx @@ -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 = ({ emoji, onPick }) => { return ( - - - ) } diff --git a/src/renderer/src/components/Buttons/ActionIconButton.tsx b/src/renderer/src/components/Buttons/ActionIconButton.tsx index 9f5c98abf0..bf3a6d288d 100644 --- a/src/renderer/src/components/Buttons/ActionIconButton.tsx +++ b/src/renderer/src/components/Buttons/ActionIconButton.tsx @@ -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' diff --git a/src/renderer/src/components/ConfirmDialog.tsx b/src/renderer/src/components/ConfirmDialog.tsx index 5ac0ae1273..a3ffa5e270 100644 --- a/src/renderer/src/components/ConfirmDialog.tsx +++ b/src/renderer/src/components/ConfirmDialog.tsx @@ -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 = ({ x, y, message, onConfirm, onCancel }) => {
{message}
- - +
diff --git a/src/renderer/src/components/ErrorBoundary.tsx b/src/renderer/src/components/ErrorBoundary.tsx index 12e9ab5935..838c6f136b 100644 --- a/src/renderer/src/components/ErrorBoundary.tsx +++ b/src/renderer/src/components/ErrorBoundary.tsx @@ -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 = (props: FallbackProps): Re type="error" action={ - - diff --git a/src/renderer/src/components/HorizontalScrollContainer/index.tsx b/src/renderer/src/components/HorizontalScrollContainer/index.tsx index fdc890d2e2..ec9f7a1043 100644 --- a/src/renderer/src/components/HorizontalScrollContainer/index.tsx +++ b/src/renderer/src/components/HorizontalScrollContainer/index.tsx @@ -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' diff --git a/src/renderer/src/components/Popups/AddAssistantOrAgentPopup.tsx b/src/renderer/src/components/Popups/AddAssistantOrAgentPopup.tsx index 4094b3f3d1..4b3f969a26 100644 --- a/src/renderer/src/components/Popups/AddAssistantOrAgentPopup.tsx +++ b/src/renderer/src/components/Popups/AddAssistantOrAgentPopup.tsx @@ -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 = ({ onSelect, resolve }) => { - - - + +
+ + +
+
- - {selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')} - + + {selectedFolderPath || t('settings.data.export_to_phone.lan.noZipSelected')} + - - - - - - {showCloseConfirm && ( - -
-
- ⚠️ - - {t('settings.data.export_to_phone.lan.confirm_close_title')} - -
- - {t('settings.data.export_to_phone.lan.confirm_close_message')} - -
- - -
-
-
- )} - - )} - + + + ) } diff --git a/src/renderer/src/components/Popups/UpdateDialogPopup.tsx b/src/renderer/src/components/Popups/UpdateDialogPopup.tsx new file mode 100644 index 0000000000..29afcc0d24 --- /dev/null +++ b/src/renderer/src/components/Popups/UpdateDialogPopup.tsx @@ -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 = ({ 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 ( + +

{t('update.title')}

+

{t('update.message').replace('{{version}}', releaseInfo?.version || '')}

+ + } + open={open} + onCancel={onCancel} + afterClose={onClose} + transitionName="animation-move-down" + centered + width={720} + footer={[ + , + + ]}> + + + + {typeof releaseNotes === 'string' + ? releaseNotes + : Array.isArray(releaseNotes) + ? releaseNotes + .map((note: ReleaseNoteInfo) => note.note) + .filter(Boolean) + .join('\n\n') + : t('update.noReleaseNotes')} + + + +
+ ) +} + +const TopViewKey = 'UpdateDialogPopup' + +export default class UpdateDialogPopup { + static topviewId = 0 + static hide() { + TopView.hide(TopViewKey) + } + static show(props: ShowParams) { + return new Promise((resolve) => { + TopView.show( + { + 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; + } + } +` diff --git a/src/renderer/src/components/Popups/agent/AgentModal.tsx b/src/renderer/src/components/Popups/agent/AgentModal.tsx index cb53879fcc..d504699399 100644 --- a/src/renderer/src/components/Popups/agent/AgentModal.tsx +++ b/src/renderer/src/components/Popups/agent/AgentModal.tsx @@ -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 = ({ 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 = ({ 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(() => 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 = ({ agent, isOpen: _isOpen, onClose: _ ...prev, configuration: { ...parsedConfiguration, - permission_mode: nextMode + permission_mode: value } } }) @@ -150,55 +115,57 @@ export const AgentModal: React.FC = ({ agent, isOpen: _isOpen, onClose: _ [] ) - const agentOptions: AgentTypeOption[] = useMemo( + const agentOptions = useMemo( () => - agentConfig.map( - (option) => - ({ - ...option, - rendered: