Files
Agent-System-Frontend/src/components/ProjectNav.tsx
T
weichen 298c41ff67 Make the right rail pure inbox and move deep views into overlay workspaces (T4)
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
2026-09-30 08:04:21 +00:00

75 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 左栏:项目折叠组 → 会话列表;底部账户/设置入口(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>
</>
)
}