Why: D25 final state - the right rail answers only "what needs me now" (inbox + a pinned current-task card); artifacts/agents/plugin panels become overlay workspaces with Cmd shortcuts and a left-rail quick section; DocViewer/AgentTracker migrate onto the shared OverlayHost. Evidence: tsc clean; vitest 9/9; vite build ok; delegation-only logic (overlay semantics covered by ui OverlayHost suite). Impact: Workbench drops classic tabs and gains CurrentTaskCard (plan+todos merged); new Workspaces.tsx (workspaceEntries, Cmd1-9 handler, builtin/plugin/server workspace rendering); store gains activeWorkspace/serverWorkspaces + open/close; ProjectNav quick section; Conversation project badge (D26); DocViewer/AgentTracker render through OverlayHost. Refs: docs/proposals/2026-09-30-ui-inbox-command-workspace.md T4; task card feature/workspaces-r1
75 lines
3.6 KiB
TypeScript
75 lines
3.6 KiB
TypeScript
// 左栏:项目折叠组 → 会话列表;底部账户/设置入口(4.4)。
|
||
import { useEffect, useState } from 'react'
|
||
import { store, useStore } from '../store'
|
||
import { workspaceEntries } from './Workspaces'
|
||
|
||
export function ProjectNav() {
|
||
const projects = useStore(s => s.projects)
|
||
const currentProjectId = useStore(s => s.currentProjectId)
|
||
const conversations = useStore(s => s.conversations)
|
||
const currentConvId = useStore(s => s.currentConvId)
|
||
const [collapsedProjects, setCollapsedProjects] = useState<Record<string, boolean>>({})
|
||
const [creating, setCreating] = useState(false)
|
||
const [newName, setNewName] = useState('')
|
||
|
||
useEffect(() => { setNewName(`项目 ${projects.length + 1}`) }, [projects.length])
|
||
|
||
return (
|
||
<>
|
||
<div className="nav-header">
|
||
<span className="nav-label">Agent-System</span>
|
||
<button title="新建项目" onClick={() => setCreating(v => !v)}>✚</button>
|
||
</div>
|
||
<div className="ws-quick">
|
||
<div className="nav-label">工作区</div>
|
||
<div className="ws-quick-list">
|
||
{workspaceEntries().map(e => (
|
||
<button key={e.key} className="ws-quick-item" title={`${e.title}(${e.shortcut})`}
|
||
onClick={() => store.openWorkspace(e.key)}>
|
||
<span>{e.icon}</span><span className="name">{e.title}</span><span className="sc">{e.shortcut}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
{creating && (
|
||
<div style={{ padding: '0 10px 8px', display: 'flex', gap: 6 }}>
|
||
<input className="search-input" value={newName} onChange={e => setNewName(e.target.value)}
|
||
onKeyDown={async e => {
|
||
if (e.key === 'Enter' && newName.trim()) { await store.createProject(newName.trim()); setCreating(false) }
|
||
}} placeholder="项目名称" autoFocus />
|
||
<button className="btn" onClick={async () => { if (newName.trim()) { await store.createProject(newName.trim()); setCreating(false) } }}>建</button>
|
||
</div>
|
||
)}
|
||
<div className="nav-scroll">
|
||
{projects.length === 0 && <div style={{ padding: 12, color: 'var(--text-faint)', fontSize: 12.5 }}>尚无项目——点击 ✚ 创建</div>}
|
||
{projects.map(p => {
|
||
const open = !collapsedProjects[p.id]
|
||
return (
|
||
<div className="nav-project" key={p.id}>
|
||
<div className={`nav-project-name ${open ? 'open' : ''} ${p.id === currentProjectId ? '' : ''}`}
|
||
style={p.id === currentProjectId ? { color: 'var(--text)' } : undefined}
|
||
onClick={async () => {
|
||
setCollapsedProjects(c => ({ ...c, [p.id]: !(c[p.id] ?? p.id === currentProjectId ? false : true) }))
|
||
await store.openProject(p.id)
|
||
}}>
|
||
<span className="chev">▶</span>
|
||
<span style={{ flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.name}</span>
|
||
</div>
|
||
{open && p.id === currentProjectId && conversations.map(c => (
|
||
<div key={c.id} className={`nav-conv ${c.id === currentConvId ? 'active' : ''}`}
|
||
onClick={() => store.openConversation(c.id)} title={c.title}>
|
||
{c.title}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
<div className="nav-footer">
|
||
<button className="nav-label" onClick={() => store.createConversation()}>+ 新会话</button>
|
||
<button className="nav-label" onClick={() => store.openSettings(true)}>⚙ 设置</button>
|
||
</div>
|
||
</>
|
||
)
|
||
}
|