// 左栏(方案 A「任务中心」,提案 2026-09-30 R1):单一主创建入口(P6)+ // 组织视图(P8:按项目/时间线)+ 置顶/归档 + 账户区(P7)。 // 工作区快捷图标条保留(D25:⌘ 与左栏共享同一清单)。 import { useEffect, useRef, useState } from 'react' import { store, useStore } from '../store' import { auth } from '../auth' import { api } from '../api' import { Icon, BrandMark, workspaceIcon } from '@ui' import type { Conversation } from '../contracts' import { workspaceEntries } from './Workspaces' import { groupByProject, groupByTimeline, relativeTime, filterTasks, type TaskOrg } from '../tasklist' const GROUPS_KEY = 'as.taskGroups' function loadCollapsed(): Record { try { return JSON.parse(localStorage.getItem(GROUPS_KEY) ?? '{}') } catch { return {} } } /** 小型弹出菜单:锚点下方渲染,点击外部关闭。 */ function Menu({ anchor, onClose, children }: { anchor: HTMLElement | null; onClose: () => void; children: React.ReactNode }) { const ref = useRef(null) useEffect(() => { if (!anchor || !ref.current) return const r = anchor.getBoundingClientRect() const el = ref.current let x = r.left, y = r.bottom + 6 if (x + el.offsetWidth > window.innerWidth - 8) x = window.innerWidth - el.offsetWidth - 8 if (y + el.offsetHeight > window.innerHeight - 8) y = r.top - el.offsetHeight - 6 el.style.left = `${Math.max(8, x)}px` el.style.top = `${y}px` const h = (e: MouseEvent) => { if (!el.contains(e.target as Node)) onClose() } window.addEventListener('mousedown', h) return () => window.removeEventListener('mousedown', h) }, [anchor]) if (!anchor) return null return
e.stopPropagation()}>{children}
} function MenuItem({ onClick, danger, icon, children }: { onClick: () => void; danger?: boolean; icon?: Parameters[0]['name']; children: React.ReactNode }) { return } export function TaskSidebar() { const projects = useStore(s => s.projects) const tasks = useStore(s => s.tasks) const me = useStore(s => s.me) const currentConvId = useStore(s => s.currentConvId) const currentProjectId = useStore(s => s.currentProjectId) const org = useStore(s => s.ui.taskOrg) const showArchived = useStore(s => s.ui.showArchived) const theme = useStore(s => s.ui.theme) const pendingBadge = useStore(s => s.cards.filter(c => c.status === 'action_required').length) const [q, setQ] = useState('') const [collapsed, setCollapsed] = useState>(loadCollapsed) const [menu, setMenu] = useState<{ kind: 'new' | 'filter' | 'account' | `row:${string}`; anchor: HTMLElement } | null>(null) const [creatingProject, setCreatingProject] = useState(false) const [newProjectName, setNewProjectName] = useState('') // ⌘N/Ctrl+N 新建任务(P6:创建入口唯一 + 快捷键)。 useEffect(() => { const h = (e: KeyboardEvent) => { const k = e.key.toLowerCase() if ((e.metaKey || e.ctrlKey) && k === 'n') { e.preventDefault(); store.createTask() } if ((e.metaKey || e.ctrlKey) && k === 'b') { e.preventDefault(); store.toggleLeft() } } window.addEventListener('keydown', h, true) return () => window.removeEventListener('keydown', h, true) }, []) const tglGroup = (key: string) => { setCollapsed(c => { const next = { ...c, [key]: !c[key] } try { localStorage.setItem(GROUPS_KEY, JSON.stringify(next)) } catch { /* ignore */ } return next }) } const setOrg = (o: TaskOrg) => store.setUI({ taskOrg: o }) const visible = filterTasks(tasks, q) const pinned = visible.filter(t => t.pinned) const rest = visible.filter(t => !t.pinned) const groups = org === 'project' ? groupByProject(rest, projects) : groupByTimeline(rest) const currentProject = projects.find(p => p.id === currentProjectId) const rowTask = menu?.kind.startsWith('row:') ? tasks.find(t => t.id === menu.kind.slice(4)) : null const rowMenuAnchor = menu && menu.kind.startsWith('row:') ? menu.anchor : null const rowActions = (t: Conversation) => ( ) const taskRow = (t: Conversation, badge?: string) => (
store.openTask(t.id)} onContextMenu={e => { e.preventDefault(); setMenu({ kind: `row:${t.id}`, anchor: e.currentTarget as HTMLElement }) }}> {t.title || '未命名任务'} {badge && {badge}} {relativeTime(t.updatedAt)} {rowActions(t)}
) return ( <>
Agent-System
{/* 单一主创建入口(P6):新建任务 + 次级菜单(新建项目/指定项目新建) */}
setQ(e.target.value)} />
{/* 组织视图切换(P8)+ 过滤 */}
{creatingProject && (
setNewProjectName(e.target.value)} onKeyDown={async e => { if (e.key === 'Enter' && newProjectName.trim()) { await store.createProject(newProjectName.trim()) setCreatingProject(false); setNewProjectName('') } if (e.key === 'Escape') setCreatingProject(false) }} placeholder="项目名称,回车创建" />
)} {pinned.length > 0 && ( <>
置顶
{pinned.map(t => taskRow(t, org === 'timeline' ? projects.find(p => p.id === t.projectId)?.name : undefined))} )} {groups.length === 0 && pinned.length === 0 && (
尚无任务——点击「新建任务」开始
(项目内素材上传、检索与生成全程可在对话中完成)
)} {groups.map(g => (
{org === 'project' ? (
tglGroup(g.key)}> {g.label} {g.tasks.length}
) : (
{g.label}
)} {!collapsed[g.key] && g.tasks.map(t => taskRow(t, org === 'timeline' ? projects.find(p => p.id === t.projectId)?.name : undefined))}
))}
{/* 工作区快捷图标条(D25:与 ⌘1–9 同一清单) */}
{workspaceEntries().map(e => ( ))}
{/* 账户区(P7):身份常驻;阶段 1 = API key 即身份(user 即租户 owner) */}
{pendingBadge > 0 && }
{/* 弹出菜单们 */} setMenu(null)}> { store.createTask(); setMenu(null) }}>新建任务 · 当前项目⌘N
{ setCreatingProject(true); setMenu(null) }}>新建项目… {projects.filter(p => p.id !== currentProjectId).slice(0, 6).map(p => ( { store.createTask(p.id); setMenu(null) }}>在「{p.name}」新建任务 ))}
setMenu(null)}> {rowTask && <> { store.pinTask(rowTask.id, !rowTask.pinned); setMenu(null) }}>{rowTask.pinned ? '取消置顶' : '置顶'} { store.archiveTask(rowTask.id, !rowTask.archived); setMenu(null) }}>{rowTask.archived ? '恢复(移出归档)' : '归档'}
{ store.setUI({ taskOrg: 'timeline' }); setMenu(null) }}>按时间线查看 }
setMenu(null)}>
{(me?.name || me?.owner || '?').slice(0, 1).toUpperCase()}
{me?.name || '未连接'}
{me?.owner ? `租户:${me.owner}` : '—'}
{ store.openSettings(true); setMenu(null) }}>偏好设置 { store.openInbox(); setMenu(null) }}>收件箱{pendingBadge > 0 ? `(待办 ${pendingBadge})` : ''} { store.setUI({ changePwOpen: true }); setMenu(null) }}>修改密码… {me?.isAdmin && { store.setUI({ adminUsersOpen: true }); setMenu(null) }}>用户管理…} {me?.auth && <>
{ api.logout().catch(() => undefined); auth.logout() }}>退出登录 }
) }