diff --git a/vscode/webview-ui/src/components/ManagedSessionsPanel.tsx b/vscode/webview-ui/src/components/ManagedSessionsPanel.tsx
index aead87c..a246a7d 100644
--- a/vscode/webview-ui/src/components/ManagedSessionsPanel.tsx
+++ b/vscode/webview-ui/src/components/ManagedSessionsPanel.tsx
@@ -12,6 +12,8 @@ import type { ManagedSession, ManagedSessionsData } from '../types'
import { Download, Plus, Terminal, Trash2, X } from 'lucide-react'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
+import { SelectMenu } from './ui/select-menu'
+
interface ManagedSessionsPanelProps {
data: ManagedSessionsData
profiles: ClaudeProfile[]
@@ -143,18 +145,15 @@ export function ManagedSessionsPanel({
Profile
-
+ onChange={setSelectedProfile}
+ options={profiles.length === 0
+ ? [{ value: '', label: '(无可用 profile)' }]
+ : profiles.map(p => ({ value: p.path, label: p.name }))}
+ variant="input"
+ className="min-w-0 flex-1"
+ />
名字
@@ -385,24 +384,18 @@ function SessionRow({ session, profiles, onRename, onResume, onDelete, onCloseTa
{basename(session.profilePath)}
)
: (
-
+ variant="ghost"
+ options={[
+ ...(storedMissing
+ ? [{ value: pickedProfile, label: basename(pickedProfile) || '(未设置)' }]
+ : []),
+ ...profiles.map(p => ({ value: p.path, label: p.name })),
+ ]}
+ />
)}
{created ? {(profiles.length > 0 || Boolean(basename(session.profilePath))) ? ' · ' : ''}{created} : null}
diff --git a/vscode/webview-ui/src/components/SettingsModal.tsx b/vscode/webview-ui/src/components/SettingsModal.tsx
index 728394b..fe22601 100644
--- a/vscode/webview-ui/src/components/SettingsModal.tsx
+++ b/vscode/webview-ui/src/components/SettingsModal.tsx
@@ -11,6 +11,7 @@ import type { ReactElement } from 'react'
import type { ClaudeProfile } from '../hooks/useIssues'
import { X } from 'lucide-react'
import { useEffect, useState } from 'react'
+import { SelectMenu } from './ui/select-menu'
type GroupKey = 'auth' | 'network' | 'youtrack' | 'prompts' | 'hooks'
@@ -487,22 +488,20 @@ export function SettingsModal({
>
)}
>
-
+ options={[
+ { value: '', label: '(默认)' },
+ ...profiles.map(p => ({ value: p.path, label: p.name })),
+ ...(conflictResolutionProfilePath
+ && !profiles.some(p => p.path === conflictResolutionProfilePath)
+ ? [{ value: conflictResolutionProfilePath, label: `自定义:${conflictResolutionProfilePath}` }]
+ : []),
+ ]}
+ />
)}
>
-
+ options={[
+ { value: '', label: '(未配置)' },
+ ...profiles.map(p => ({ value: p.path, label: p.name })),
+ ...(commitProfilePath
+ && !profiles.some(p => p.path === commitProfilePath)
+ ? [{ value: commitProfilePath, label: `自定义:${commitProfilePath}` }]
+ : []),
+ ]}
+ />
{youtrackProjects.status === 'ready' && youtrackProjects.projects.length > 0 && (
-
+ variant="input"
+ options={[
+ { value: '', label: '— 选择项目 —' },
+ ...youtrackProjects.projects.map(p => ({
+ value: p.shortName,
+ label: `${p.shortName} · ${p.name}`,
+ })),
+ ]}
+ />
)}
{youtrackProjects.status === 'error' && (
@@ -891,14 +887,16 @@ export function SettingsModal({
>
)}
>
-
+ variant="input"
+ options={[
+ { value: 'vscode', label: 'VS Code' },
+ { value: 'pycharm', label: 'PyCharm' },
+ ]}
+ />
{worktreeOpenWith === 'pycharm' && (
diff --git a/vscode/webview-ui/src/components/property-grid.tsx b/vscode/webview-ui/src/components/property-grid.tsx
index 2c2679e..3245996 100644
--- a/vscode/webview-ui/src/components/property-grid.tsx
+++ b/vscode/webview-ui/src/components/property-grid.tsx
@@ -9,7 +9,6 @@
* 背景色,依赖 VS Code 主题 CSS 变量。
*/
-import { useEffect, useMemo, useRef, useState } from 'react'
import type { ReactNode } from 'react'
import {
ChevronDown,
@@ -24,6 +23,8 @@ import {
Search,
Settings,
} from 'lucide-react'
+import { useEffect, useMemo, useRef, useState } from 'react'
+import { SelectMenu } from './ui/select-menu'
// --- 类型定义 ---
@@ -193,22 +194,16 @@ function PropertyRow({
)
}
return (
-
+ onChange={v => onChange(propDef.key, v)}
+ className={inputClass}
+ variant="input"
+ options={(propDef.options ?? []).map(o => ({
+ value: String(o.value),
+ label: o.label,
+ }))}
+ />
)
case 'multiline':
diff --git a/vscode/webview-ui/src/components/ui/select-menu.tsx b/vscode/webview-ui/src/components/ui/select-menu.tsx
new file mode 100644
index 0000000..ccb022c
--- /dev/null
+++ b/vscode/webview-ui/src/components/ui/select-menu.tsx
@@ -0,0 +1,256 @@
+import type { KeyboardEvent, MouseEvent, ReactElement } from 'react'
+import { Check, ChevronDown } from 'lucide-react'
+import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
+import { createPortal } from 'react-dom'
+import { cn } from '@/lib/utils'
+
+export interface SelectMenuOption {
+ value: string
+ label: string
+ hint?: string
+}
+
+export interface SelectMenuProps {
+ value: string
+ options: SelectMenuOption[]
+ onChange: (value: string) => void
+ placeholder?: string
+ title?: string
+ disabled?: boolean
+ variant?: 'input' | 'ghost'
+ className?: string
+}
+
+const MAX_MENU_HEIGHT = 240
+
+/**
+ * 自定义下拉组件,替代原生 select。
+ *
+ * 原生 select 的展开弹层是 Chromium 系统层,VS Code 暗色主题下样式脱节,
+ * 这里用手写弹层 + createPortal 渲染到 body,避免被 overflow 容器裁掉。
+ */
+export function SelectMenu({
+ value,
+ options,
+ onChange,
+ placeholder = '请选择',
+ title,
+ disabled = false,
+ variant = 'input',
+ className,
+}: SelectMenuProps): ReactElement {
+ const [open, setOpen] = useState(false)
+ const [highlight, setHighlight] = useState(-1)
+ const [pos, setPos] = useState<{ top: number, left: number, width: number } | null>(null)
+ const triggerRef = useRef(null)
+ const menuRef = useRef(null)
+ const optionRefs = useRef>([])
+
+ const selected = options.find(o => o.value === value) ?? null
+
+ const computePos = useCallback((): void => {
+ const el = triggerRef.current
+ if (!el)
+ return
+ const rect = el.getBoundingClientRect()
+ const menuHeight = Math.min(options.length * 32, MAX_MENU_HEIGHT)
+ const spaceBelow = window.innerHeight - rect.bottom
+ const spaceAbove = rect.top
+ const openUp = spaceBelow < menuHeight && spaceAbove > spaceBelow
+ setPos({
+ top: openUp ? Math.max(8, rect.top - menuHeight - 4) : rect.bottom + 4,
+ left: rect.left,
+ width: Math.max(rect.width, 120),
+ })
+ }, [options.length])
+
+ const openMenu = useCallback((): void => {
+ computePos()
+ setOpen(true)
+ setHighlight(options.findIndex(o => o.value === value))
+ }, [computePos, options, value])
+
+ const closeMenu = useCallback((): void => {
+ setOpen(false)
+ setHighlight(-1)
+ }, [])
+
+ const selectOption = useCallback((index: number): void => {
+ const opt = options[index]
+ if (opt)
+ onChange(opt.value)
+ closeMenu()
+ }, [options, onChange, closeMenu])
+
+ const moveHighlight = useCallback((dir: 1 | -1): void => {
+ if (options.length === 0)
+ return
+ setHighlight((prev) => {
+ if (prev < 0)
+ return dir === 1 ? 0 : options.length - 1
+ return (prev + dir + options.length) % options.length
+ })
+ }, [options.length])
+
+ // 弹层打开时把高亮项滚进视野(初始定位 + 键盘移动都靠它)。
+ useLayoutEffect(() => {
+ if (!open)
+ return
+ const el = optionRefs.current[highlight]
+ el?.scrollIntoView({ block: 'nearest' })
+ }, [open, highlight])
+
+ // 外点击关闭:mousedown 捕获阶段,落在触发器/弹层内不关。
+ useEffect(() => {
+ if (!open)
+ return
+ function onPointerDown(e: globalThis.MouseEvent): void {
+ const t = e.target
+ if (t instanceof Node && !triggerRef.current?.contains(t) && !menuRef.current?.contains(t))
+ setOpen(false)
+ }
+ document.addEventListener('mousedown', onPointerDown, true)
+ return () => document.removeEventListener('mousedown', onPointerDown, true)
+ }, [open])
+
+ // 窗口 resize / 容器滚动时重算位置,让弹层始终贴住触发器。
+ useEffect(() => {
+ if (!open)
+ return
+ function onScroll(): void {
+ computePos()
+ }
+ window.addEventListener('resize', onScroll)
+ document.addEventListener('scroll', onScroll, true)
+ return () => {
+ window.removeEventListener('resize', onScroll)
+ document.removeEventListener('scroll', onScroll, true)
+ }
+ }, [open, computePos])
+
+ function onTriggerClick(e: MouseEvent): void {
+ e.stopPropagation()
+ if (disabled)
+ return
+ if (open)
+ closeMenu()
+ else
+ openMenu()
+ }
+
+ function onTriggerMouseDown(e: MouseEvent): void {
+ e.stopPropagation()
+ }
+
+ function onTriggerKeyDown(e: KeyboardEvent): void {
+ if (disabled)
+ return
+ const key = e.key
+ if (!open) {
+ if (key === 'ArrowDown' || key === 'ArrowUp' || key === 'Enter' || key === ' ') {
+ e.preventDefault()
+ openMenu()
+ }
+ return
+ }
+ switch (key) {
+ case 'ArrowDown':
+ e.preventDefault()
+ e.stopPropagation()
+ moveHighlight(1)
+ break
+ case 'ArrowUp':
+ e.preventDefault()
+ e.stopPropagation()
+ moveHighlight(-1)
+ break
+ case 'Enter':
+ case ' ':
+ e.preventDefault()
+ e.stopPropagation()
+ if (highlight >= 0)
+ selectOption(highlight)
+ break
+ case 'Escape':
+ e.preventDefault()
+ e.stopPropagation()
+ closeMenu()
+ break
+ }
+ }
+
+ const triggerClasses
+ = variant === 'input'
+ ? 'rounded border border-[var(--vscode-input-border,var(--vscode-panel-border))] bg-[var(--vscode-input-background)] px-2 py-1 text-xs text-[var(--vscode-input-foreground)] focus:ring-1 focus:ring-inset focus:ring-[var(--vscode-focusBorder)]'
+ : 'rounded bg-transparent px-1 py-0 text-[10px] text-[var(--vscode-descriptionForeground)] hover:bg-[var(--vscode-toolbar-hoverBackground)]'
+
+ return (
+ <>
+
+ {open && pos && createPortal(
+
+ {options.length === 0
+ ? (
+
+ 无选项
+
+ )
+ : options.map((opt, i) => (
+
+ ))}
+
,
+ document.body,
+ )}
+ >
+ )
+}