diff --git a/packages/ui/MIGRATION_STATUS.md b/packages/ui/MIGRATION_STATUS.md
index 57eb5b5329..cf17765677 100644
--- a/packages/ui/MIGRATION_STATUS.md
+++ b/packages/ui/MIGRATION_STATUS.md
@@ -49,9 +49,9 @@ function MyComponent() {
## 迁移概览
- **总组件数**: 236
-- **已迁移**: 36
+- **已迁移**: 38
- **已重构**: 0
-- **待迁移**: 200
+- **待迁移**: 198
## 组件状态表
@@ -74,6 +74,7 @@ function MyComponent() {
| | ExpandableText | ✅ | ❌ | 可展开文本 |
| | ThinkingEffect | ✅ | ❌ | 思考效果动画 |
| | EmojiAvatar | ✅ | ❌ | 表情头像 |
+| | ListItem | ✅ | ❌ | 列表项 |
| | CodeViewer | ❌ | ❌ | 代码查看器 (外部依赖) |
| | OGCard | ❌ | ❌ | OG 卡片 |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown 渲染器 |
@@ -105,8 +106,8 @@ function MyComponent() {
| | InfoTooltip | ✅ | ❌ | 信息提示 |
| | HelpTooltip | ✅ | ❌ | 帮助提示 |
| | WarnTooltip | ✅ | ❌ | 警告提示 |
+| | EditableNumber | ✅ | ❌ | 可编辑数字 |
| | DraggableList | ❌ | ❌ | 可拖拽列表 |
-| | EditableNumber | ❌ | ❌ | 可编辑数字 |
| | EmojiPicker | ❌ | ❌ | 表情选择器 |
| | Selector | ✅ | ❌ | 选择器 (i18n 依赖) |
| | ModelSelector | ❌ | ❌ | 模型选择器 (Redux 依赖) |
diff --git a/packages/ui/MIGRATION_STATUS_EN.md b/packages/ui/MIGRATION_STATUS_EN.md
index 60c041204c..ad07d752a8 100644
--- a/packages/ui/MIGRATION_STATUS_EN.md
+++ b/packages/ui/MIGRATION_STATUS_EN.md
@@ -48,9 +48,9 @@ When submitting PRs, please place components in the correct directory based on t
## Migration Overview
- **Total Components**: 236
-- **Migrated**: 36
+- **Migrated**: 38
- **Refactored**: 0
-- **Pending Migration**: 200
+- **Pending Migration**: 198
## Component Status Table
@@ -73,6 +73,7 @@ When submitting PRs, please place components in the correct directory based on t
| | ExpandableText | ✅ | ❌ | Expandable text |
| | ThinkingEffect | ✅ | ❌ | Thinking effect animation |
| | EmojiAvatar | ✅ | ❌ | Emoji avatar |
+| | ListItem | ✅ | ❌ | List item |
| | CodeViewer | ❌ | ❌ | Code viewer (external deps) |
| | OGCard | ❌ | ❌ | OG card |
| | MarkdownShadowDOMRenderer | ❌ | ❌ | Markdown renderer |
@@ -104,8 +105,8 @@ When submitting PRs, please place components in the correct directory based on t
| | InfoTooltip | ✅ | ❌ | Info tooltip |
| | HelpTooltip | ✅ | ❌ | Help tooltip |
| | WarnTooltip | ✅ | ❌ | Warning tooltip |
+| | EditableNumber | ✅ | ❌ | Editable number |
| | DraggableList | ❌ | ❌ | Draggable list |
-| | EditableNumber | ❌ | ❌ | Editable number |
| | EmojiPicker | ❌ | ❌ | Emoji picker |
| | Selector | ✅ | ❌ | Selector (i18n dependency) |
| | ModelSelector | ❌ | ❌ | Model selector (Redux dependency) |
diff --git a/packages/ui/src/components/display/ListItem/index.tsx b/packages/ui/src/components/display/ListItem/index.tsx
new file mode 100644
index 0000000000..344dfbadf0
--- /dev/null
+++ b/packages/ui/src/components/display/ListItem/index.tsx
@@ -0,0 +1,92 @@
+// Original path: src/renderer/src/components/ListItem/index.tsx
+import { Typography } from 'antd'
+import { ReactNode } from 'react'
+import styled from 'styled-components'
+
+interface ListItemProps {
+ active?: boolean
+ icon?: ReactNode
+ title: ReactNode
+ subtitle?: string
+ titleStyle?: React.CSSProperties
+ onClick?: () => void
+ rightContent?: ReactNode
+ style?: React.CSSProperties
+}
+
+const ListItem = ({ active, icon, title, subtitle, titleStyle, onClick, rightContent, style }: ListItemProps) => {
+ return (
+
+
+ {icon && {icon}}
+
+
+ {title}
+
+ {subtitle && {subtitle}}
+
+ {rightContent && {rightContent}}
+
+
+ )
+}
+
+const ListItemContainer = styled.div`
+ padding: 7px 12px;
+ border-radius: var(--list-item-border-radius);
+ font-size: 13px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ position: relative;
+ cursor: pointer;
+ border: 1px solid transparent;
+
+ &:hover {
+ background-color: var(--color-background-soft);
+ }
+
+ &.active {
+ background-color: var(--color-background-soft);
+ border: 1px solid var(--color-border-soft);
+ }
+`
+
+const ListItemContent = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ overflow: hidden;
+ font-size: 13px;
+`
+
+const IconWrapper = styled.span`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 8px;
+`
+
+const TextContainer = styled.div`
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+`
+
+const SubtitleText = styled.div`
+ font-size: 10px;
+ color: var(--color-text-soft);
+ margin-top: 2px;
+ display: -webkit-box;
+ -webkit-line-clamp: 1;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ color: var(--color-text-3);
+`
+
+const RightContentWrapper = styled.div`
+ margin-left: auto;
+`
+
+export default ListItem
diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts
index 3e9133eea1..75b1b63587 100644
--- a/packages/ui/src/components/index.ts
+++ b/packages/ui/src/components/index.ts
@@ -15,6 +15,7 @@ export { WarnTag } from './base/WarnTag'
export { default as Ellipsis } from './display/Ellipsis'
export { default as EmojiAvatar } from './display/EmojiAvatar'
export { default as ExpandableText } from './display/ExpandableText'
+export { default as ListItem } from './display/ListItem'
export { default as ThinkingEffect } from './display/ThinkingEffect'
// Layout Components
@@ -39,6 +40,8 @@ export { default as WebSearchIcon } from './icons/WebSearchIcon'
export { default as WrapIcon } from './icons/WrapIcon'
// Interactive Components
+export type { EditableNumberProps } from './interactive/EditableNumber'
+export { default as EditableNumber } from './interactive/EditableNumber'
export { default as HelpTooltip } from './interactive/HelpTooltip'
export { default as InfoTooltip } from './interactive/InfoTooltip'
export { default as Selector } from './interactive/Selector'
diff --git a/packages/ui/src/components/interactive/EditableNumber/index.tsx b/packages/ui/src/components/interactive/EditableNumber/index.tsx
new file mode 100644
index 0000000000..e335df8160
--- /dev/null
+++ b/packages/ui/src/components/interactive/EditableNumber/index.tsx
@@ -0,0 +1,116 @@
+// Original path: src/renderer/src/components/EditableNumber/index.tsx
+import { InputNumber } from 'antd'
+import { FC, useEffect, useRef, useState } from 'react'
+import styled from 'styled-components'
+
+export interface EditableNumberProps {
+ value?: number | null
+ min?: number
+ max?: number
+ step?: number
+ precision?: number
+ placeholder?: string
+ disabled?: boolean
+ changeOnBlur?: boolean
+ onChange?: (value: number | null) => void
+ onBlur?: () => void
+ style?: React.CSSProperties
+ className?: string
+ size?: 'small' | 'middle' | 'large'
+ suffix?: string
+ prefix?: string
+ align?: 'start' | 'center' | 'end'
+}
+
+const EditableNumber: FC = ({
+ value,
+ min,
+ max,
+ step = 0.01,
+ precision,
+ placeholder,
+ disabled = false,
+ onChange,
+ onBlur,
+ changeOnBlur = false,
+ style,
+ className,
+ size = 'middle',
+ align = 'end'
+}) => {
+ const [isEditing, setIsEditing] = useState(false)
+ const [inputValue, setInputValue] = useState(value)
+ const inputRef = useRef(null)
+
+ useEffect(() => {
+ setInputValue(value)
+ }, [value])
+
+ const handleFocus = () => {
+ if (disabled) return
+ setIsEditing(true)
+ }
+
+ const handleInputChange = (newValue: number | null) => {
+ onChange?.(newValue ?? null)
+ }
+
+ const handleBlur = () => {
+ setIsEditing(false)
+ onBlur?.()
+ }
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ handleBlur()
+ } else if (e.key === 'Escape') {
+ e.stopPropagation()
+ setInputValue(value)
+ setIsEditing(false)
+ }
+ }
+
+ return (
+
+
+
+ {value ?? placeholder}
+
+
+ )
+}
+
+const Container = styled.div`
+ display: inline-block;
+ position: relative;
+`
+
+const DisplayText = styled.div<{
+ $align: 'start' | 'center' | 'end'
+ $isEditing: boolean
+}>`
+ position: absolute;
+ inset: 0;
+ display: ${({ $isEditing }) => ($isEditing ? 'none' : 'flex')};
+ align-items: center;
+ justify-content: ${({ $align }) => $align};
+ pointer-events: none;
+`
+
+export default EditableNumber