feat(vscode): 会话管理支持导入已 /rename 的命名会话

This commit is contained in:
2026-07-24 00:12:28 +08:00
parent 7c67d220db
commit dc522064ce
10 changed files with 438 additions and 9 deletions
@@ -9,7 +9,7 @@
import type { ClaudeProfile } from '../hooks/useIssues'
import type { ManagedSession, ManagedSessionsData } from '../types'
import { Plus, Terminal, Trash2, X } from 'lucide-react'
import { Download, Plus, Terminal, Trash2, X } from 'lucide-react'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
interface ManagedSessionsPanelProps {
@@ -20,6 +20,10 @@ interface ManagedSessionsPanelProps {
onResume: (id: string) => void
onDelete: (id: string) => void
onCloseTab: (id: string) => void
importableSessions: Array<{ id: string, name: string, mtimeMs: number }>
importableLoading: boolean
onListImportable: () => void
onImport: (sessionId: string, name: string) => void
}
function basename(p?: string): string {
@@ -45,10 +49,24 @@ function formatTime(ts: number): string {
}
}
export function ManagedSessionsPanel({ data, profiles, onCreate, onRename, onResume, onDelete, onCloseTab }: ManagedSessionsPanelProps) {
export function ManagedSessionsPanel({
data,
profiles,
onCreate,
onRename,
onResume,
onDelete,
onCloseTab,
importableSessions,
importableLoading,
onListImportable,
onImport,
}: ManagedSessionsPanelProps) {
const [selectedProfile, setSelectedProfile] = useState<string>('')
const [name, setName] = useState<string>('')
const [prompt, setPrompt] = useState<string>('')
const [importOpen, setImportOpen] = useState(false)
const importWrapRef = useRef<HTMLDivElement | null>(null)
// 默认选中官方 profile(profiles 异步到达后;用户手动改选后不覆盖)。
useEffect(() => {
@@ -58,6 +76,29 @@ export function ManagedSessionsPanel({ data, profiles, onCreate, onRename, onRes
setSelectedProfile((official ?? profiles[0]).path)
}, [profiles, selectedProfile])
// 弹层:外点击 / Esc 关闭
useEffect(() => {
if (!importOpen)
return
function onKey(e: KeyboardEvent): void {
if (e.key === 'Escape')
setImportOpen(false)
}
function onPointer(e: MouseEvent): void {
const el = importWrapRef.current
if (!el)
return
if (e.target instanceof Node && !el.contains(e.target))
setImportOpen(false)
}
window.addEventListener('keydown', onKey)
window.addEventListener('mousedown', onPointer)
return () => {
window.removeEventListener('keydown', onKey)
window.removeEventListener('mousedown', onPointer)
}
}, [importOpen])
const sessions = useMemo(
() => [...data.sessions].sort((a, b) => b.createdAt - a.createdAt),
[data.sessions],
@@ -75,6 +116,16 @@ export function ManagedSessionsPanel({ data, profiles, onCreate, onRename, onRes
setPrompt('')
}, [canCreate, name, prompt, selectedProfile, onCreate])
const openImport = useCallback((): void => {
setImportOpen(true)
onListImportable()
}, [onListImportable])
const pickImport = useCallback((sessionId: string, sessionName: string): void => {
onImport(sessionId, sessionName)
setImportOpen(false)
}, [onImport])
return (
<div className="flex h-full w-full flex-col overflow-hidden bg-[var(--vscode-editor-background)] text-[var(--vscode-foreground)]">
{/* 上方表单 */}
@@ -97,13 +148,65 @@ export function ManagedSessionsPanel({ data, profiles, onCreate, onRename, onRes
</div>
<div className="flex items-center gap-2">
<span className="w-12 shrink-0 text-xs text-[var(--vscode-descriptionForeground)]"></span>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
placeholder="会话名字(可选)"
className="min-w-0 flex-1 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)] outline-none placeholder:text-[var(--vscode-input-placeholderForeground)] focus:ring-1 focus:ring-inset focus:ring-[var(--vscode-focusBorder)]"
/>
<div className="relative min-w-0 flex-1" ref={importWrapRef}>
<div className="flex items-center gap-1">
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
placeholder="会话名字(可选)"
className="min-w-0 flex-1 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)] outline-none placeholder:text-[var(--vscode-input-placeholderForeground)] focus:ring-1 focus:ring-inset focus:ring-[var(--vscode-focusBorder)]"
/>
<button
type="button"
onClick={openImport}
title="导入已 /rename 的命名会话"
className="flex shrink-0 items-center gap-1 rounded border border-[var(--vscode-input-border,var(--vscode-panel-border))] bg-[var(--vscode-button-secondaryBackground,var(--vscode-input-background))] px-2 py-1 text-xs text-[var(--vscode-button-secondaryForeground,var(--vscode-foreground))] hover:bg-[var(--vscode-button-secondaryHoverBackground,var(--vscode-toolbar-hoverBackground))]"
>
<Download className="size-3.5" />
<span></span>
</button>
</div>
{importOpen && (
<div
className="absolute left-0 right-0 top-full z-20 mt-1 max-h-48 overflow-auto rounded border border-[var(--vscode-panel-border)] bg-[var(--vscode-editorWidget-background,var(--vscode-editor-background))] shadow-md"
role="listbox"
aria-label="可导入的命名会话"
>
{importableLoading
? (
<div className="px-2 py-3 text-center text-xs text-[var(--vscode-descriptionForeground)]">
</div>
)
: importableSessions.length === 0
? (
<div className="px-2 py-3 text-center text-xs text-[var(--vscode-descriptionForeground)]">
</div>
)
: (
<ul className="flex flex-col py-1">
{importableSessions.map(s => (
<li key={s.id}>
<button
type="button"
role="option"
onClick={() => pickImport(s.id, s.name)}
className="flex w-full flex-col items-start gap-0.5 px-2 py-1.5 text-left hover:bg-[var(--vscode-list-hoverBackground)]"
>
<span className="truncate text-xs">{s.name}</span>
<span className="truncate text-[10px] text-[var(--vscode-descriptionForeground)]">
{formatTime(s.mtimeMs)}
</span>
</button>
</li>
))}
</ul>
)}
</div>
)}
</div>
</div>
<div className="flex items-start gap-2">
<span className="w-12 shrink-0 pt-1 text-xs text-[var(--vscode-descriptionForeground)]"></span>